Compare commits
196 Commits
feature/#2
...
feature/im
Author | SHA1 | Date | |
---|---|---|---|
5ac056bb8a | |||
664a74c973 | |||
0c77758351 | |||
e8ef160f2a | |||
2d6831b4ef | |||
0464538b1c | |||
e61aeed691 | |||
45e756fcd3 | |||
7c473de12b | |||
5982422e04 | |||
04e081c31a | |||
599264b362 | |||
6f84238503 | |||
586bb0ca83 | |||
465219276d | |||
11d30351ba | |||
85af73c079 | |||
9c28b10383 | |||
495e9f192e | |||
30b2028bd8 | |||
9d6a8730a9 | |||
ba12674e7c | |||
a5c941cbb0 | |||
0f017cfe10 | |||
c9cc5be519 | |||
ce073a67af | |||
cb6fcbcb8e | |||
ba0ceda03a | |||
4745fb5145 | |||
30cce5845c | |||
2d43b3f1d2 | |||
045c693329 | |||
ff01e41c8f | |||
8781bf43a1 | |||
1223ae42ef | |||
6e58de8840 | |||
f5c4f3df19 | |||
e1ff2fefe1 | |||
0bc81ba4cc | |||
baf0350102 | |||
5c94584cb2 | |||
4f1b9cf024 | |||
0b99d4098e | |||
5b386ae455 | |||
3da21a7856 | |||
e1a6f650fb | |||
6dda79f8b2 | |||
918f5141fc | |||
bd3bf6f580 | |||
bd85908014 | |||
474683082d | |||
0fe060ff99 | |||
30dc69ab4b | |||
343c67a110 | |||
5d6cb478cd | |||
a95c67b5fe | |||
e571cf2230 | |||
b7f448cb17 | |||
4a963b4359 | |||
691abb7c4f | |||
6a27ccff31 | |||
9d72995225 | |||
2de2bec705 | |||
bf64a6df70 | |||
1b87f1dd91 | |||
f4746722af | |||
f5a7a348e0 | |||
d70e25207b | |||
4dd71a25b5 | |||
5c87b7b4af | |||
aa3ee8f0af | |||
95f4c58110 | |||
b4989aac26 | |||
c35e27799e | |||
125d3a3f66 | |||
d299528c26 | |||
bc58d41c54 | |||
058988e874 | |||
72562f92f9 | |||
38395b6f19 | |||
88cc8f5b08 | |||
413a5cbcf5 | |||
cfae96bde8 | |||
42e7b7312e | |||
f76b758565 | |||
434f5df7c2 | |||
5990b6d6ac | |||
8377fe6545 | |||
8980691409 | |||
40c24cee10 | |||
e5df80647f | |||
75595515b5 | |||
e19f30b15a | |||
af5f4f97f2 | |||
241cfb3eb2 | |||
ac4cefa902 | |||
3dffad928d | |||
ce90d6f7a9 | |||
b520acc2db | |||
75333d2659 | |||
dd9e039649 | |||
1facd2d641 | |||
5af2e399c9 | |||
6e3c97d7d1 | |||
bf58fc4944 | |||
68f7db7aa4 | |||
bbcd122a6c | |||
23c437f0bc | |||
eb2648d31f | |||
7e8fcc766a | |||
5c47edd230 | |||
2c10b54582 | |||
2ad58aea9f | |||
bd8caaf27c | |||
3cf86e322c | |||
6f32fbdc79 | |||
743d4594df | |||
2be49c010f | |||
2ac9416fe6 | |||
43fe6ab33e | |||
1cbf116ad4 | |||
3f10b03d24 | |||
a525d80530 | |||
4748044ab3 | |||
3b0138130b | |||
54c75896f9 | |||
9467797dc9 | |||
a8934f8e40 | |||
65cae5d824 | |||
179ccdbc55 | |||
ff39628f0c | |||
d4680b198e | |||
550b961505 | |||
1839bd9a22 | |||
1017013032 | |||
d5c7cd0294 | |||
4a62bbb118 | |||
40c7f6289a | |||
72d731c6f2 | |||
fc92d9ea79 | |||
2e267a36aa | |||
6ee8bb8334 | |||
ec3bf0f51e | |||
27f8bc8784 | |||
3185c478a6 | |||
72ef04d683 | |||
446e8fa617 | |||
f7072acdd2 | |||
fda8cc532e | |||
821e742527 | |||
460308d555 | |||
3f8f8745eb | |||
bf7f585270 | |||
fee4277b4f | |||
ffc07b7403 | |||
344ddbaf39 | |||
86ed3ae4b0 | |||
719c75616e | |||
cf954979c5 | |||
01ed1bce29 | |||
d4e0cbe398 | |||
628b3bf1fa | |||
709d34d59b | |||
c4a42066ab | |||
26dbaa45a7 | |||
ae0241fecb | |||
3a566dae5a | |||
ad4f33676f | |||
44cfbd6ee8 | |||
881e3375ab | |||
6a76c4797a | |||
bf75ad001b | |||
3b473e5826 | |||
929a36554a | |||
3fbc5f4e87 | |||
1526e0947a | |||
7ec4303b40 | |||
f475b69022 | |||
27d8c7cff6 | |||
b9a7f9aa8e | |||
5b6b968541 | |||
93abf4b631 | |||
0de574b9e1 | |||
c04c52aed0 | |||
3f19730bd8 | |||
d0e3c95bb0 | |||
82f51b2b7e | |||
1b9db64854 | |||
41c71d5964 | |||
bd04dc2ab8 | |||
a4e96f9ede | |||
f6bac403a2 | |||
8460d0b535 | |||
5a36d10f0e | |||
8f8f019ab7 | |||
6a1823586a |
18
.env.example
18
.env.example
@ -1,9 +1,17 @@
|
||||
# Server configuration
|
||||
ENV=development
|
||||
HOST="0.0.0.0"
|
||||
PORT=4000
|
||||
DATABASE_URL="mysql://root@localhost:3306/nq"
|
||||
REDIS_URL="redis://@127.0.0.1:6379/4"
|
||||
JWT_SECRET="secret"
|
||||
CLIENT_URL="http://192.168.3.4:5173"
|
||||
|
||||
# Database configuration
|
||||
REDIS_URL="redis://@127.0.0.1:6379/4"
|
||||
DB_HOST="localhost"
|
||||
DB_USER="root"
|
||||
DB_PASS=""
|
||||
DB_PORT="3306"
|
||||
DB_NAME="game"
|
||||
|
||||
# Game configuration
|
||||
ALLOW_DIAGONAL_MOVEMENT=false
|
||||
@ -12,3 +20,9 @@ ALLOW_DIAGONAL_MOVEMENT=false
|
||||
DEFAULT_CHARACTER_ZONE="0"
|
||||
DEFAULT_CHARACTER_POS_X="0"
|
||||
DEFAULT_CHARACTER_POS_Y="0"
|
||||
|
||||
# Email configuration
|
||||
SMTP_HOST=my.directonline.io
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=no-reply@noxious.gg
|
||||
SMTP_PASSWORD=""
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -309,4 +309,8 @@ $RECYCLE.BIN/
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# MikroORM
|
||||
temp/**
|
||||
migrations/*.json
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows
|
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Noxious game server
|
||||
|
||||
This is the server for the Noxious game.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies with `npm install`
|
||||
3. Copy the `.env.example` file to `.env` and fill in the required variables
|
||||
4. Run the server with `npm run dev`
|
||||
|
||||
## Commands
|
||||
|
||||
### `npm run dev`
|
||||
|
||||
Starts the server in development mode.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the server for production.
|
||||
|
||||
### `npm run format`
|
||||
|
||||
Formats the code using Prettier.
|
||||
|
||||
## MikroORM
|
||||
|
||||
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.
|
||||
|
||||
### Create migrations
|
||||
|
||||
Run `npx mikro-orm migration:create` to create a new migration.
|
||||
|
||||
### Apply migrations
|
||||
|
||||
Run `npx mikro-orm migration:up` to apply all pending migrations.
|
42
eslint.config.js
Normal file
42
eslint.config.js
Normal file
@ -0,0 +1,42 @@
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||
import tsparser from '@typescript-eslint/parser';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
|
||||
export default [
|
||||
eslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
ecmaVersion: 2023,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint,
|
||||
'import': importPlugin,
|
||||
},
|
||||
rules: {
|
||||
...tseslint.configs['recommended'].rules,
|
||||
...tseslint.configs['recommended-requiring-type-checking'].rules,
|
||||
'import/order': ['error', {
|
||||
'groups': [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
['parent', 'sibling'],
|
||||
'index',
|
||||
'object',
|
||||
'type'
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
'alphabetize': {
|
||||
'order': 'asc',
|
||||
'caseInsensitive': true
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
];
|
104
migrations/Migration20250101224501.ts
Normal file
104
migrations/Migration20250101224501.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250101224501 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` int not null default 0, \`origin_y\` int not null default 0, \`is_animated\` tinyint(1) not null default false, \`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 \`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) null, \`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, 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\` int not null default 0, \`origin_y\` int not null default 0, \`is_animated\` tinyint(1) not null default false, \`is_looping\` tinyint(1) not null default false, \`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 \`world\` (\`date\` datetime not null, \`is_rain_enabled\` tinyint(1) not null default false, \`rain_percentage\` int not null default 0, \`is_fog_enabled\` tinyint(1) not null default false, \`fog_density\` int not null default 0, primary key (\`date\`)) default character set utf8mb4 engine = InnoDB;`);
|
||||
|
||||
this.addSql(`create table \`zone\` (\`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 \`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', \`zone_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_zone_id_index\`(\`zone_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, \`zone_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_zone_id_index\`(\`zone_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 \`zone_effect\` (\`id\` varchar(255) not null, \`zone_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 \`zone_effect\` add index \`zone_effect_zone_id_index\`(\`zone_id\`);`);
|
||||
|
||||
this.addSql(`create table \`zone_event_tile\` (\`id\` varchar(255) not null, \`zone_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 \`zone_event_tile\` add index \`zone_event_tile_zone_id_index\`(\`zone_id\`);`);
|
||||
|
||||
this.addSql(`create table \`zone_event_tile_teleport\` (\`id\` varchar(255) not null, \`zone_event_tile_id\` varchar(255) not null, \`to_zone_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 \`zone_event_tile_teleport\` add unique \`zone_event_tile_teleport_zone_event_tile_id_unique\`(\`zone_event_tile_id\`);`);
|
||||
this.addSql(`alter table \`zone_event_tile_teleport\` add index \`zone_event_tile_teleport_to_zone_id_index\`(\`to_zone_id\`);`);
|
||||
|
||||
this.addSql(`create table \`zone_object\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`depth\` int not null default 0, \`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 \`zone_object\` add index \`zone_object_zone_id_index\`(\`zone_id\`);`);
|
||||
this.addSql(`alter table \`zone_object\` add index \`zone_object_map_object_id_index\`(\`map_object_id\`);`);
|
||||
|
||||
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_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`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_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`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;`);
|
||||
|
||||
this.addSql(`alter table \`zone_effect\` add constraint \`zone_effect_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
|
||||
|
||||
this.addSql(`alter table \`zone_event_tile\` add constraint \`zone_event_tile_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
|
||||
|
||||
this.addSql(`alter table \`zone_event_tile_teleport\` add constraint \`zone_event_tile_teleport_zone_event_tile_id_foreign\` foreign key (\`zone_event_tile_id\`) references \`zone_event_tile\` (\`id\`) on update cascade on delete cascade;`);
|
||||
this.addSql(`alter table \`zone_event_tile_teleport\` add constraint \`zone_event_tile_teleport_to_zone_id_foreign\` foreign key (\`to_zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
|
||||
|
||||
this.addSql(`alter table \`zone_object\` add constraint \`zone_object_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
|
||||
this.addSql(`alter table \`zone_object\` add constraint \`zone_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`);
|
||||
}
|
||||
|
||||
}
|
27
mikro-orm.config.ts
Normal file
27
mikro-orm.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb'
|
||||
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'
|
||||
|
||||
export default defineConfig({
|
||||
extensions: [Migrator],
|
||||
metadataProvider: TsMorphMetadataProvider,
|
||||
entities: ['./src/entities/**/*.js'],
|
||||
entitiesTs: ['./src/entities/**/*.ts'],
|
||||
driver: MySqlDriver,
|
||||
host: serverConfig.DB_HOST,
|
||||
port: serverConfig.DB_PORT,
|
||||
user: serverConfig.DB_USER,
|
||||
password: serverConfig.DB_PASS,
|
||||
dbName: serverConfig.DB_NAME,
|
||||
// debug: serverConfig.ENV !== 'production',
|
||||
driverOptions: {
|
||||
allowPublicKeyRetrieval: true
|
||||
},
|
||||
migrations: {
|
||||
path: './migrations',
|
||||
pathTs: './migrations',
|
||||
}
|
||||
})
|
5266
package-lock.json
generated
5266
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@ -1,22 +1,33 @@
|
||||
{
|
||||
"useTsNode": true,
|
||||
"alwaysAllowTs": true,
|
||||
"scripts": {
|
||||
"start": "npx prisma migrate deploy && node dist/server.js",
|
||||
"dev": "nodemon --ignore 'data/*' --exec ts-node src/server.ts",
|
||||
"start": "node dist/server.js",
|
||||
"dev": "nodemon --exec tsx src/server.ts",
|
||||
"build": "tsc",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.17.0",
|
||||
"@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",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@types/blessed": "^0.1.25",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"blessed": "^0.1.81",
|
||||
"bullmq": "^5.13.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.9.15",
|
||||
"pino": "^9.3.2",
|
||||
"prisma": "^5.17.0",
|
||||
"sharp": "^0.33.4",
|
||||
"socket.io": "^4.7.5",
|
||||
"ts-node": "^10.9.2",
|
||||
@ -24,11 +35,20 @@
|
||||
"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",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.3"
|
||||
"prettier": "^3.3.3",
|
||||
"prisma": "^6.1.0",
|
||||
"tsx": "^4.19.2"
|
||||
}
|
||||
}
|
||||
|
0
prisma/.gitignore
vendored
0
prisma/.gitignore
vendored
@ -1,239 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `Chat` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`characterId` INTEGER NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`message` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Sprite` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `SpriteAction` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`spriteId` VARCHAR(191) NOT NULL,
|
||||
`action` VARCHAR(191) NOT NULL,
|
||||
`sprites` JSON NULL,
|
||||
`originX` DECIMAL(65, 30) NOT NULL DEFAULT 0,
|
||||
`originY` DECIMAL(65, 30) NOT NULL DEFAULT 0,
|
||||
`isAnimated` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isLooping` BOOLEAN NOT NULL DEFAULT false,
|
||||
`frameWidth` INTEGER NOT NULL DEFAULT 0,
|
||||
`frameHeight` INTEGER NOT NULL DEFAULT 0,
|
||||
`frameSpeed` INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `User` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(191) NOT NULL,
|
||||
`password` VARCHAR(191) NOT NULL,
|
||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
UNIQUE INDEX `User_username_key`(`username`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CharacterType` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`gender` ENUM('MALE', 'FEMALE') NOT NULL,
|
||||
`race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL,
|
||||
`spriteId` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Character` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`userId` INTEGER NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
||||
`hitpoints` INTEGER NOT NULL DEFAULT 100,
|
||||
`mana` INTEGER NOT NULL DEFAULT 100,
|
||||
`level` INTEGER NOT NULL DEFAULT 1,
|
||||
`experience` INTEGER NOT NULL DEFAULT 0,
|
||||
`alignment` INTEGER NOT NULL DEFAULT 50,
|
||||
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
|
||||
`positionX` INTEGER NOT NULL DEFAULT 0,
|
||||
`positionY` INTEGER NOT NULL DEFAULT 0,
|
||||
`rotation` INTEGER NOT NULL DEFAULT 0,
|
||||
`zoneId` INTEGER NOT NULL DEFAULT 1,
|
||||
`characterTypeId` INTEGER NULL,
|
||||
|
||||
UNIQUE INDEX `Character_name_key`(`name`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CharacterItem` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`characterId` INTEGER NOT NULL,
|
||||
`itemId` VARCHAR(191) NOT NULL,
|
||||
`quantity` INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Tile` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`tags` JSON NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Object` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`tags` JSON NULL,
|
||||
`originX` DECIMAL(65, 30) NOT NULL DEFAULT 0,
|
||||
`originY` DECIMAL(65, 30) NOT NULL DEFAULT 0,
|
||||
`isAnimated` BOOLEAN NOT NULL DEFAULT false,
|
||||
`frameSpeed` INTEGER NOT NULL DEFAULT 0,
|
||||
`frameWidth` INTEGER NOT NULL DEFAULT 0,
|
||||
`frameHeight` INTEGER NOT NULL DEFAULT 0,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Item` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`description` VARCHAR(191) NULL,
|
||||
`stackable` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Zone` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`width` INTEGER NOT NULL DEFAULT 10,
|
||||
`height` INTEGER NOT NULL DEFAULT 10,
|
||||
`tiles` JSON NULL,
|
||||
`pvp` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ZoneEffect` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`effect` VARCHAR(191) NOT NULL,
|
||||
`strength` INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ZoneObject` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`objectId` VARCHAR(191) NOT NULL,
|
||||
`depth` INTEGER NOT NULL DEFAULT 0,
|
||||
`isRotated` BOOLEAN NOT NULL DEFAULT false,
|
||||
`positionX` INTEGER NOT NULL DEFAULT 0,
|
||||
`positionY` INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ZoneEventTile` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`type` ENUM('BLOCK', 'TELEPORT', 'NPC', 'ITEM') NOT NULL,
|
||||
`positionX` INTEGER NOT NULL,
|
||||
`positionY` INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ZoneEventTileTeleport` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneEventTileId` VARCHAR(191) NOT NULL,
|
||||
`toZoneId` INTEGER NOT NULL,
|
||||
`toRotation` INTEGER NOT NULL,
|
||||
`toPositionX` INTEGER NOT NULL,
|
||||
`toPositionY` INTEGER NOT NULL,
|
||||
|
||||
UNIQUE INDEX `ZoneEventTileTeleport_zoneEventTileId_key`(`zoneEventTileId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Chat` ADD CONSTRAINT `Chat_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterTypeId_fkey` FOREIGN KEY (`characterTypeId`) REFERENCES `CharacterType`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneEffect` ADD CONSTRAINT `ZoneEffect_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_objectId_fkey` FOREIGN KEY (`objectId`) REFERENCES `Object`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneEventTile` ADD CONSTRAINT `ZoneEventTile_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneEventTileTeleport` ADD CONSTRAINT `ZoneEventTileTeleport_zoneEventTileId_fkey` FOREIGN KEY (`zoneEventTileId`) REFERENCES `ZoneEventTile`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneEventTileTeleport` ADD CONSTRAINT `ZoneEventTileTeleport_toZoneId_fkey` FOREIGN KEY (`toZoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "mysql"
|
@ -1,31 +0,0 @@
|
||||
// CHEAT SHEET
|
||||
// 1. Create a new Prisma project
|
||||
// npx prisma init
|
||||
// 2. Create a new database schema
|
||||
// npx prisma db push
|
||||
// 3. Generate Prisma Client and type-safe models based on schema
|
||||
// npx prisma generate
|
||||
// 4. Create a new migration
|
||||
// npx prisma migrate dev --name [migration-name]
|
||||
// 5. Apply the migration
|
||||
// npx prisma migrate deploy
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["prismaSchemaFolder"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id Int @id @default(autoincrement())
|
||||
characterId Int
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
zoneId Int
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
message String
|
||||
createdAt DateTime
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
model Sprite {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
spriteActions SpriteAction[]
|
||||
characterTypes CharacterType[]
|
||||
}
|
||||
|
||||
model SpriteAction {
|
||||
id String @id @default(uuid())
|
||||
spriteId String
|
||||
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
action String
|
||||
sprites Json?
|
||||
originX Decimal @default(0)
|
||||
originY Decimal @default(0)
|
||||
isAnimated Boolean @default(false)
|
||||
isLooping Boolean @default(false)
|
||||
frameWidth Int @default(0)
|
||||
frameHeight Int @default(0)
|
||||
frameSpeed Int @default(0)
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
online Boolean @default(false)
|
||||
characters Character[]
|
||||
}
|
||||
|
||||
enum CharacterGender {
|
||||
MALE
|
||||
FEMALE
|
||||
}
|
||||
|
||||
enum CharacterRace {
|
||||
HUMAN
|
||||
ELF
|
||||
DWARF
|
||||
ORC
|
||||
GOBLIN
|
||||
}
|
||||
|
||||
model CharacterType {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
gender CharacterGender
|
||||
race CharacterRace
|
||||
characters Character[]
|
||||
spriteId String?
|
||||
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Character {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String @unique
|
||||
online Boolean @default(false)
|
||||
hitpoints Int @default(100)
|
||||
mana Int @default(100)
|
||||
level Int @default(1)
|
||||
experience Int @default(0)
|
||||
alignment Int @default(50)
|
||||
role String @default("player")
|
||||
positionX Int @default(0)
|
||||
positionY Int @default(0)
|
||||
rotation Int @default(0)
|
||||
zoneId Int @default(1)
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
characterTypeId Int?
|
||||
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
|
||||
chats Chat[]
|
||||
items CharacterItem[]
|
||||
}
|
||||
|
||||
model CharacterItem {
|
||||
id Int @id @default(autoincrement())
|
||||
characterId Int
|
||||
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
||||
itemId String
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
quantity Int
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
model Tile {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
tags Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Object {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
tags Json?
|
||||
originX Decimal @default(0)
|
||||
originY Decimal @default(0)
|
||||
isAnimated Boolean @default(false)
|
||||
frameSpeed Int @default(0)
|
||||
frameWidth Int @default(0)
|
||||
frameHeight Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ZoneObject ZoneObject[]
|
||||
}
|
||||
|
||||
model Item {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
description String?
|
||||
stackable Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
characters CharacterItem[]
|
||||
}
|
||||
|
||||
model Zone {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
width Int @default(10)
|
||||
height Int @default(10)
|
||||
tiles Json?
|
||||
pvp Boolean @default(false)
|
||||
zoneEffects ZoneEffect[]
|
||||
zoneEventTiles ZoneEventTile[]
|
||||
zoneEventTileTeleports ZoneEventTileTeleport[]
|
||||
zoneObjects ZoneObject[]
|
||||
characters Character[]
|
||||
chats Chat[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ZoneEffect {
|
||||
id String @id @default(uuid())
|
||||
zoneId Int
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
effect String
|
||||
strength Int
|
||||
}
|
||||
|
||||
model ZoneObject {
|
||||
id String @id @default(uuid())
|
||||
zoneId Int
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
objectId String
|
||||
object Object @relation(fields: [objectId], references: [id], onDelete: Cascade)
|
||||
depth Int @default(0)
|
||||
isRotated Boolean @default(false)
|
||||
positionX Int @default(0)
|
||||
positionY Int @default(0)
|
||||
}
|
||||
|
||||
enum ZoneEventTileType {
|
||||
BLOCK
|
||||
TELEPORT
|
||||
NPC
|
||||
ITEM
|
||||
}
|
||||
|
||||
model ZoneEventTile {
|
||||
id String @id @default(uuid())
|
||||
zoneId Int
|
||||
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
|
||||
type ZoneEventTileType
|
||||
positionX Int
|
||||
positionY Int
|
||||
teleport ZoneEventTileTeleport?
|
||||
}
|
||||
|
||||
model ZoneEventTileTeleport {
|
||||
id String @id @default(uuid())
|
||||
zoneEventTileId String @unique
|
||||
zoneEventTile ZoneEventTile @relation(fields: [zoneEventTileId], references: [id], onDelete: Cascade)
|
||||
toZoneId Int
|
||||
toZone Zone @relation(fields: [toZoneId], references: [id], onDelete: Cascade)
|
||||
toRotation Int
|
||||
toPositionX Int
|
||||
toPositionY Int
|
||||
}
|
3
public/.gitignore
vendored
3
public/.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
!.gitignore
|
||||
**
|
||||
!.gitignore
|
||||
!assets.zip
|
||||
|
BIN
public/assets.zip
Normal file
BIN
public/assets.zip
Normal file
Binary file not shown.
8
src/application/base/baseCommand.ts
Normal file
8
src/application/base/baseCommand.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import Logger, { LoggerType } from '#application/logger'
|
||||
|
||||
export abstract class BaseCommand {
|
||||
protected readonly logger = Logger.type(LoggerType.COMMAND)
|
||||
constructor(readonly io: Server) {}
|
||||
}
|
22
src/application/base/baseController.ts
Normal file
22
src/application/base/baseController.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import Logger, { LoggerType } from '#application/logger'
|
||||
|
||||
export abstract class BaseController {
|
||||
protected readonly logger = Logger.type(LoggerType.HTTP)
|
||||
|
||||
protected sendSuccess(res: Response, data?: any, message?: string, status: number = 200) {
|
||||
return res.status(status).json({
|
||||
success: true,
|
||||
message,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
protected sendError(res: Response, message: string, status: number = 400) {
|
||||
return res.status(status).json({
|
||||
success: false,
|
||||
message
|
||||
})
|
||||
}
|
||||
}
|
44
src/application/base/baseEntity.ts
Normal file
44
src/application/base/baseEntity.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { EntityManager } from '@mikro-orm/core'
|
||||
|
||||
import Database from '#application/database'
|
||||
import Logger, { LoggerType } from '#application/logger'
|
||||
|
||||
export abstract class BaseEntity {
|
||||
protected readonly logger = Logger.type(LoggerType.ENTITY)
|
||||
|
||||
private getEntityManager(): EntityManager {
|
||||
return Database.getEntityManager()
|
||||
}
|
||||
|
||||
async save(): Promise<this> {
|
||||
return this.execute('persist', 'save entity')
|
||||
}
|
||||
|
||||
async update(): Promise<this> {
|
||||
return this.execute('merge', 'update entity')
|
||||
}
|
||||
|
||||
async delete(): Promise<this> {
|
||||
return this.execute('remove', 'remove entity')
|
||||
}
|
||||
|
||||
private async execute(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise<this> {
|
||||
try {
|
||||
const em = this.getEntityManager()
|
||||
|
||||
await em.begin()
|
||||
try {
|
||||
em[method](this)
|
||||
await em.flush()
|
||||
await em.commit()
|
||||
return this
|
||||
} catch (error) {
|
||||
await em.rollback()
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
24
src/application/base/baseEvent.ts
Normal file
24
src/application/base/baseEvent.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import Logger, { LoggerType } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
|
||||
export abstract class BaseEvent {
|
||||
protected readonly logger = Logger.type(LoggerType.GAME)
|
||||
|
||||
constructor(
|
||||
readonly io: Server,
|
||||
readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
protected emitError(message: string): void {
|
||||
this.socket.emit('notification', { title: 'Server message', message })
|
||||
this.logger.error('character:connect 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)
|
||||
}
|
||||
}
|
13
src/application/base/baseRepository.ts
Normal file
13
src/application/base/baseRepository.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { EntityManager } from '@mikro-orm/core'
|
||||
|
||||
import Database from '../database'
|
||||
|
||||
import Logger, { LoggerType } from '#application/logger'
|
||||
|
||||
export abstract class BaseRepository {
|
||||
protected readonly logger = Logger.type(LoggerType.REPOSITORY)
|
||||
|
||||
protected get em(): EntityManager {
|
||||
return Database.getEntityManager()
|
||||
}
|
||||
}
|
5
src/application/base/baseService.ts
Normal file
5
src/application/base/baseService.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Logger, { LoggerType } from '#application/logger'
|
||||
|
||||
export abstract class BaseService {
|
||||
protected readonly logger = Logger.type(LoggerType.GAME)
|
||||
}
|
36
src/application/config.ts
Normal file
36
src/application/config.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
class config {
|
||||
// Server configuration
|
||||
static ENV: string = process.env.ENV || 'development'
|
||||
static HOST: string = process.env.HOST || '0.0.0.0'
|
||||
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
|
||||
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
|
||||
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://noxious.gg'
|
||||
|
||||
// Database configuration
|
||||
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
|
||||
static DB_HOST: string = process.env.DB_HOST || 'localhost'
|
||||
static DB_USER: string = process.env.DB_USER || 'root'
|
||||
static DB_PASS: string = process.env.DB_PASS || ''
|
||||
static DB_PORT: number = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306
|
||||
static DB_NAME: string = process.env.DB_NAME || 'game'
|
||||
|
||||
// Game configuration
|
||||
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
|
||||
|
||||
// Default character create values
|
||||
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
|
||||
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
|
||||
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
|
||||
|
||||
// Email configuration
|
||||
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
|
||||
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
|
||||
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@noxious.gg'
|
||||
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
|
||||
}
|
||||
|
||||
export default config
|
38
src/application/database.ts
Normal file
38
src/application/database.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { EntityManager } from '@mikro-orm/core'
|
||||
import { MikroORM } from '@mikro-orm/mysql'
|
||||
|
||||
import Logger, { LoggerType } from './logger'
|
||||
import config from '../../mikro-orm.config'
|
||||
|
||||
class Database {
|
||||
private static orm: MikroORM
|
||||
private static em: EntityManager
|
||||
private static logger = Logger.type(LoggerType.APP)
|
||||
|
||||
public static async initialize(): Promise<void> {
|
||||
try {
|
||||
Database.orm = await MikroORM.init(config)
|
||||
Database.em = Database.orm.em.fork()
|
||||
this.logger.info('Database connection initialized')
|
||||
} catch (error) {
|
||||
this.logger.error(`MikroORM connection failed: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public static getORM(): MikroORM {
|
||||
if (!Database.orm) {
|
||||
throw new Error('Database not initialized. Call Database.initialize() first.')
|
||||
}
|
||||
return Database.orm
|
||||
}
|
||||
|
||||
public static getEntityManager(): EntityManager {
|
||||
if (!Database.em) {
|
||||
throw new Error('Database not initialized. Call Database.initialize() first.')
|
||||
}
|
||||
return Database.em
|
||||
}
|
||||
}
|
||||
|
||||
export default Database
|
63
src/application/enums.ts
Normal file
63
src/application/enums.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export enum SocketEvent {
|
||||
CHARACTER_CONNECT = 1,
|
||||
CHARACTER_MOVE = 2,
|
||||
CHARACTER_MOVE_ERROR = 3,
|
||||
CHARACTER_TELEPORT = 4,
|
||||
ZONE_CHARACTER_LEAVE = 5,
|
||||
ZONE_CHARACTER_JOIN = 6,
|
||||
ZONE_CHARACTER_LIST = 7,
|
||||
ZONE_CHARACTER_DELETE = 8,
|
||||
ZONE_CHARACTER_CREATE = 9,
|
||||
ZONE_CHARACTER_UPDATE = 10,
|
||||
ZONE_CHARACTER_HAIR_UPDATE = 11,
|
||||
ZONE_CHARACTER_HAIR_LIST = 12,
|
||||
ZONE_CHARACTER_TELEPORT = 13
|
||||
}
|
||||
|
||||
export enum ItemType {
|
||||
WEAPON = 'WEAPON',
|
||||
HELMET = 'HELMET',
|
||||
CHEST = 'CHEST',
|
||||
LEGS = 'LEGS',
|
||||
BOOTS = 'BOOTS',
|
||||
GLOVES = 'GLOVES',
|
||||
RING = 'RING',
|
||||
NECKLACE = 'NECKLACE'
|
||||
}
|
||||
|
||||
export enum ItemRarity {
|
||||
COMMON = 'COMMON',
|
||||
UNCOMMON = 'UNCOMMON',
|
||||
RARE = 'RARE',
|
||||
EPIC = 'EPIC',
|
||||
LEGENDARY = 'LEGENDARY'
|
||||
}
|
||||
|
||||
export enum CharacterGender {
|
||||
MALE = 'MALE',
|
||||
FEMALE = 'FEMALE'
|
||||
}
|
||||
|
||||
export enum CharacterRace {
|
||||
HUMAN = 'HUMAN',
|
||||
ELF = 'ELF',
|
||||
DWARF = 'DWARF',
|
||||
ORC = 'ORC',
|
||||
GOBLIN = 'GOBLIN'
|
||||
}
|
||||
|
||||
export enum CharacterEquipmentSlotType {
|
||||
HEAD = 'HEAD',
|
||||
BODY = 'BODY',
|
||||
ARMS = 'ARMS',
|
||||
LEGS = 'LEGS',
|
||||
NECK = 'NECK',
|
||||
RING = 'RING'
|
||||
}
|
||||
|
||||
export enum ZoneEventTileType {
|
||||
BLOCK = 'BLOCK',
|
||||
TELEPORT = 'TELEPORT',
|
||||
NPC = 'NPC',
|
||||
ITEM = 'ITEM'
|
||||
}
|
52
src/application/logger.ts
Normal file
52
src/application/logger.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import pino from 'pino'
|
||||
|
||||
export enum LoggerType {
|
||||
HTTP = 'http',
|
||||
GAME = 'game',
|
||||
GAME_MASTER = 'gameMaster',
|
||||
APP = 'app',
|
||||
QUEUE = 'queue',
|
||||
COMMAND = 'command',
|
||||
REPOSITORY = 'repository',
|
||||
ENTITY = 'entity',
|
||||
CONSOLE = 'console'
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private instances: Map<LoggerType, ReturnType<typeof pino>> = new Map()
|
||||
|
||||
private getLogger(type: LoggerType): ReturnType<typeof pino> {
|
||||
if (!this.instances.has(type)) {
|
||||
this.instances.set(
|
||||
type,
|
||||
pino({
|
||||
level: process.env.LOG_LEVEL || 'debug',
|
||||
transport: {
|
||||
target: 'pino/file',
|
||||
options: {
|
||||
destination: `./logs/${type}.log`,
|
||||
mkdir: true
|
||||
}
|
||||
},
|
||||
formatters: {
|
||||
level: (label) => ({ level: label.toUpperCase() })
|
||||
},
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
base: null
|
||||
})
|
||||
)
|
||||
}
|
||||
return this.instances.get(type)!
|
||||
}
|
||||
|
||||
type(type: LoggerType) {
|
||||
return {
|
||||
info: (message: string, ...args: any[]) => this.getLogger(type).info(message, ...args),
|
||||
error: (message: string, ...args: any[]) => this.getLogger(type).error(message, ...args),
|
||||
warn: (message: string, ...args: any[]) => this.getLogger(type).warn(message, ...args),
|
||||
debug: (message: string, ...args: any[]) => this.getLogger(type).debug(message, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Logger()
|
71
src/application/storage.ts
Normal file
71
src/application/storage.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import config from '#application/config'
|
||||
|
||||
class Storage {
|
||||
private readonly baseDir: string
|
||||
private readonly rootDir: string
|
||||
|
||||
constructor() {
|
||||
this.rootDir = process.cwd()
|
||||
this.baseDir = config.ENV === 'development' ? 'src' : 'dist'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets path relative to project root
|
||||
*/
|
||||
public getRootPath(folder: string, ...additionalSegments: string[]): string {
|
||||
return path.join(this.rootDir, folder, ...additionalSegments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets path relative to app directory (src/dist)
|
||||
*/
|
||||
public getAppPath(folder: string, ...additionalSegments: string[]): string {
|
||||
return path.join(this.rootDir, this.baseDir, folder, ...additionalSegments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets path relative to public directory
|
||||
*/
|
||||
public getPublicPath(folder: string, ...additionalSegments: string[]): string {
|
||||
return path.join(this.rootDir, 'public', folder, ...additionalSegments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path exists
|
||||
* @throws Error if path is empty or invalid
|
||||
*/
|
||||
public doesPathExist(pathToCheck: string): boolean {
|
||||
if (!pathToCheck) {
|
||||
throw new Error('Path cannot be empty')
|
||||
}
|
||||
|
||||
try {
|
||||
fs.accessSync(pathToCheck, fs.constants.F_OK)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory and any necessary parent directories
|
||||
* @throws Error if directory creation fails
|
||||
*/
|
||||
public createDir(dirPath: string): void {
|
||||
if (!dirPath) {
|
||||
throw new Error('Directory path cannot be empty')
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(dirPath, { recursive: true })
|
||||
} catch (error) {
|
||||
const typedError = error as Error
|
||||
throw new Error(`Failed to create directory: ${typedError.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Storage()
|
63
src/application/types.ts
Normal file
63
src/application/types.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Server, Socket } from 'socket.io'
|
||||
|
||||
import { Character } from '#entities/character'
|
||||
import { ZoneEventTile } from '#entities/zoneEventTile'
|
||||
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
|
||||
|
||||
export type UUID = `${string}-${string}-${string}-${string}-${string}`
|
||||
|
||||
export type TSocket = Socket & {
|
||||
userId?: UUID
|
||||
characterId?: UUID
|
||||
handshake?: {
|
||||
query?: {
|
||||
token?: any
|
||||
}
|
||||
}
|
||||
request?: {
|
||||
headers?: {
|
||||
cookie?: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ExtendedCharacter = Character & {
|
||||
isMoving?: boolean
|
||||
resetMovement?: boolean
|
||||
}
|
||||
|
||||
export type ZoneEventTileWithTeleport = ZoneEventTile & {
|
||||
teleport: ZoneEventTileTeleport
|
||||
}
|
||||
|
||||
export type AssetData = {
|
||||
key: string
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
originX?: number
|
||||
originY?: number
|
||||
isAnimated?: boolean
|
||||
frameRate?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
frameCount?: number
|
||||
}
|
||||
|
||||
export type WorldSettings = {
|
||||
date: Date
|
||||
isRainEnabled: boolean
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
new (io: Server): {
|
||||
execute(args: string[]): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
// export type TCharacter = Socket & {
|
||||
// user?: User
|
||||
// character?: Character
|
||||
// }
|
3
src/application/utilities.ts
Normal file
3
src/application/utilities.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function unduplicateArray(array: any[]) {
|
||||
return [...new Set(array.flat())]
|
||||
}
|
@ -20,6 +20,29 @@ export const registerAccountSchema = z.object({
|
||||
.min(3, { message: 'Name must be at least 3 characters long' })
|
||||
.max(255, { message: 'Name must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
|
||||
email: z
|
||||
.string()
|
||||
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
||||
message: 'Password must be at least 8 characters long'
|
||||
})
|
||||
.max(255)
|
||||
})
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
|
||||
})
|
||||
|
||||
export const newPasswordSchema = z.object({
|
||||
urlToken: z.string().min(10, { message: 'Invalid request' }).max(255, { message: 'Invalid request' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
@ -1,10 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { BaseCommand } from '#application/base/baseCommand'
|
||||
|
||||
type CommandInput = string[]
|
||||
|
||||
export default class AlertCommand {
|
||||
constructor(private readonly io: Server) {}
|
||||
|
||||
export default class AlertCommand extends BaseCommand {
|
||||
public execute(input: CommandInput): void {
|
||||
const message: string = input.join(' ') ?? null
|
||||
if (!message) return console.log('message is required')
|
||||
|
255
src/commands/init.ts
Normal file
255
src/commands/init.ts
Normal file
@ -0,0 +1,255 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import sharp from 'sharp'
|
||||
|
||||
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'
|
||||
import { MapObject } from '#entities/mapObject'
|
||||
import { Sprite } from '#entities/sprite'
|
||||
import { SpriteAction } from '#entities/spriteAction'
|
||||
import { Tile } from '#entities/tile'
|
||||
import { User } from '#entities/user'
|
||||
import { Zone } from '#entities/zone'
|
||||
import { ZoneEffect } from '#entities/zoneEffect'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import CharacterTypeRepository from '#repositories/characterTypeRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
// @TODO : Replace this with seeding
|
||||
// https://mikro-orm.io/docs/seeding
|
||||
|
||||
export default class InitCommand extends BaseCommand {
|
||||
public async execute(): Promise<void> {
|
||||
// Assets
|
||||
await this.importTiles()
|
||||
await this.importObjects()
|
||||
await this.createCharacterType()
|
||||
await this.createCharacterHair()
|
||||
// await this.createCharacterEquipment()
|
||||
|
||||
// Zone
|
||||
await this.createZone()
|
||||
|
||||
// User
|
||||
await this.createUser()
|
||||
|
||||
// Stop process
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
private async importTiles(): Promise<void> {
|
||||
for (const tile of fs.readdirSync(Storage.getPublicPath('tiles'))) {
|
||||
const newTile = new Tile()
|
||||
newTile.setId(tile.split('.')[0] as UUID).setName('New tile')
|
||||
|
||||
await newTile.save()
|
||||
}
|
||||
}
|
||||
|
||||
private async importObjects(): Promise<void> {
|
||||
for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) {
|
||||
const newMapObject = new MapObject()
|
||||
newMapObject
|
||||
.setId(object.split('.')[0] as UUID)
|
||||
.setName('New object')
|
||||
.setFrameWidth(
|
||||
(await sharp(Storage.getPublicPath('objects', object))
|
||||
.metadata()
|
||||
.then((metadata) => metadata.height)) ?? 0
|
||||
)
|
||||
.setFrameHeight(
|
||||
(await sharp(Storage.getPublicPath('objects', object))
|
||||
.metadata()
|
||||
.then((metadata) => metadata.width)) ?? 0
|
||||
)
|
||||
|
||||
await newMapObject.save()
|
||||
}
|
||||
}
|
||||
|
||||
private async createCharacterType(): Promise<void> {
|
||||
const characterSprite = new Sprite()
|
||||
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Character')
|
||||
await characterSprite.save()
|
||||
|
||||
const idleRightDownAction = new SpriteAction()
|
||||
await idleRightDownAction
|
||||
.setAction('idle_right_down')
|
||||
.setSprites([
|
||||
'data:image/png;base64,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'
|
||||
])
|
||||
.setOriginX(0)
|
||||
.setOriginY(0)
|
||||
.setIsAnimated(false)
|
||||
.setIsLooping(false)
|
||||
.setFrameWidth(64)
|
||||
.setFrameHeight(94)
|
||||
.setFrameRate(0)
|
||||
.setSprite(characterSprite)
|
||||
.save()
|
||||
|
||||
const idleLeftUpAction = new SpriteAction()
|
||||
await idleLeftUpAction
|
||||
.setAction('idle_left_up')
|
||||
.setSprites([
|
||||
'data:image/png;base64,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'
|
||||
])
|
||||
.setOriginX(0)
|
||||
.setOriginY(0)
|
||||
.setIsAnimated(false)
|
||||
.setIsLooping(false)
|
||||
.setFrameWidth(64)
|
||||
.setFrameHeight(94)
|
||||
.setFrameRate(0)
|
||||
.setSprite(characterSprite)
|
||||
.save()
|
||||
|
||||
const walkRightDownAction = new SpriteAction()
|
||||
await walkRightDownAction
|
||||
.setAction('walk_right_down')
|
||||
.setSprites([
|
||||
'data:image/png;base64,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',
|
||||
'data:image/png;base64,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',
|
||||
'data:image/png;base64,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',
|
||||
'data:image/png;base64,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'
|
||||
])
|
||||
.setOriginX(0)
|
||||
.setOriginY(0)
|
||||
.setIsAnimated(true)
|
||||
.setIsLooping(false)
|
||||
.setFrameWidth(64)
|
||||
.setFrameHeight(94)
|
||||
.setFrameRate(7)
|
||||
.setSprite(characterSprite)
|
||||
.save()
|
||||
|
||||
const walkLeftUpAction = new SpriteAction()
|
||||
await walkLeftUpAction
|
||||
.setAction('walk_left_up')
|
||||
.setSprites([
|
||||
'data:image/png;base64,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',
|
||||
'data:image/png;base64,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',
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABZCAYAAADVeL+8AAAFcElEQVR4nLVZzUtbWRT/JUqURAkkIWlBeIUmuDHQrqQI3Ygu/Qtk6LLMUldDqYtAF10ozKrMLjDC7LNsEMSdm7GQboIRfBDQ99BHxQ/aQM0sknM9971z33upmQOB5H78zu/ee+75uEks//EXoiTXqPWldm9tKxE2bzIO6MbqIqzqvNZ3tHeA+rDfpMQInmvU+huri6gsr2Lm+ROt7+bkHC+XXyNXLMFzHdQbtb6kQATPNWr9Pzd/w3RlAelCBhP5itY/A+AGgFUFLAxWJClImhinipYIrCZmZ5EqWoM5xZI8RmqsLK8imZ0VJ0hiVefx5sWzwMFr4MQ6TH5eHuPu4nYwOYJAYM+t6ryadHdxizSOtX4CjiOhphgFdn91DQCwW22xX9xzmhRXPNfBq7e/R4ObWJgIHO0diMAiuH9yWN/hP38r4O/HXwPjAnvuuU4sBYBu39KKReY91w4FpTHkb74ff9VIkWjMvbWtRL1R6+eKJVhVfWCqaAWU2q02LANrI3PPdSIPlvrtVltkLYJ7a1uJ+pfTgAJ+G6ndcx0jsJn5UAEHkoC51L+cBvy60RS9ta3EzudDIysOKgGHgo8ipkgUG3xUlwAACX+A5j6Z3K8/fpIc7R0AkPc7AJ5r1PpvXjxTN8+qzgfsm6IPtZEpkgFwJZMSMDFNFa1BOMNAAf1OFzK4Gc6jOCoFa+2GSsDpQgZ3AOAOxqQLGQADu7+/ulYrkYJ1krPmwCQT+YoC5G1cZp4/Qe7VoiJIoqyFGqVrz8Ek4Il8BRP5CqYrC1qwVuDcIvgB/rzUY6i/7ebkXP1OFzLaqjU7t1tt7Hw+DLDnYH5lPdfGzcm5WlUyO6t2QYFzjaMIEZFWOJbr7xfa4kjwuHnK3cVtgH0s5mEKRvbnJCZnZVLmb49kblLA200BfeQDlVhHBuieaxv3z88+rm8fmymSQu6HjOCmfZRYe66DnmurSoQu5KOYR2VmY9kWikIjgcfJGcPEmM5xBaSEK+u5dmTKZ2RumsiVRUkA/LI0j+2zqchkNCwBJRELrpWVFWw3m8DZN3xAUAGBbp9NAQDyQ2UUR2llxmpufX0djuPgXbMp9E4pEgDw7+4DAe52RfBSqYRyuYxyuYylpSUAQKfTEUk4jnlrRPByuSwO9rd3Oh2USnLdD7AD9R/e3Nyc+pgUmkgQlmiKBOj/7VcYJQo8yqyiCJBwf58EHsoUk48g6Xa7xj7Pdcxhjqe++/v7AVCylm63qz4mCyIJWMvm0x/Y3t0F8HBgBCKB+U2RF7xifv6hCLwbKiChC0OmR6DNZhN5BmxkzvPz3apeNYgXdbjSOiNvt9qqjNHA+dvW/dX1oERvAcApNp/+UAQAPSvmNSu3Os3OqZKg78CDiXJL4kUCT2BplWQcGjgFWH8lAehlOwEns7NIZmfx6eN7VezyV7rINy7OxP+qMZ1dUKv89PE9i06nD9tClsJLElPiQ2W73Wor6+BjxbKFREriRSXD7SFgKiUBBCsLXoXRNTaFOdp/u9VWMZUXw6RYYx6XtZ89MfcTCmwLdz6jekoSMgINnIDjZrH+1+fQS3R/da0dUFwxVYKhT3+mpw5ezoeVmGIF/RgxXn8uUXmgsuWQN/SxpNB+JxcA/5Xy3D8nVyxpjivgcklGtXGJ3CSdPAGTZzNZCpeeayMF/c1LA6cv6UJmpP8jPNcZRqm23uYH99+0qC0hv05+W+pX4L9i41FbBvxP7y2h4Ka/w8YCPi4RwcflZ4zMpT/yHgWeLmQwXVlAqmjh5fJrbKwuPkqBAh/lAsUVdUPvr64HD8LjBrdbbVhVaH4iToUcCe6tbSV2GrX+BgDyE6bH9lFFvfz7D+4xoCT/AaNCPNJBtKgMAAAAAElFTkSuQmCC',
|
||||
'data:image/png;base64,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'
|
||||
])
|
||||
.setOriginX(0)
|
||||
.setOriginY(0)
|
||||
.setIsAnimated(true)
|
||||
.setIsLooping(false)
|
||||
.setFrameWidth(64)
|
||||
.setFrameHeight(94)
|
||||
.setFrameRate(7)
|
||||
.setSprite(characterSprite)
|
||||
.save()
|
||||
|
||||
const characterType = new CharacterType()
|
||||
await characterType.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0').setName('New character type').setGender(CharacterGender.MALE).setRace(CharacterRace.HUMAN).setIsSelectable(true).setSprite(characterSprite).save()
|
||||
}
|
||||
|
||||
private async createCharacterHair(): Promise<void> {
|
||||
const hairSprite = new Sprite()
|
||||
hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1')
|
||||
await hairSprite.save()
|
||||
|
||||
const frontAction = new SpriteAction()
|
||||
await frontAction
|
||||
.setAction('front')
|
||||
.setSprites([
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA5ElEQVR4nK2VMQ7DIAxFc4AulViysTBUldgqReoGp+pReloqIhx9LOy4IoOVYBw//DFkyZ/vUs3db2Vkuc1rMTwOrRvUwPjwZXs9S0rv/d1BkuDXYx4N41SABAl+7ZJXPzcEqQALJApmroAnC22smbQPw5WPkjoAE7w++X6JAJSF9JRWS3EWiEnzwCrBWPJLkA4gdU1sPvoQIdjGGE+xp8mTULr1IO5Orjlt9NkGaif4kIgqwC6hSarMkkgFoLZ8hVvTeQoglXwJQNMV5bsUUJNKnZJnAHhVWO76vwCjH8hs8gr4ARPESmbfQUg4AAAAAElFTkSuQmCC'
|
||||
])
|
||||
.setOriginX(0.5)
|
||||
.setOriginY(5.34)
|
||||
.setIsAnimated(false)
|
||||
.setIsLooping(false)
|
||||
.setFrameWidth(64)
|
||||
.setFrameHeight(18)
|
||||
.setFrameRate(0)
|
||||
.setSprite(hairSprite)
|
||||
.save()
|
||||
|
||||
const backAction = new SpriteAction()
|
||||
await backAction
|
||||
.setAction('back')
|
||||
.setSprites([
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAWCAYAAADafVyIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABHUlEQVR4nJ1VuwrDMAzMB3QpZOmWxUMpdCsYsrlflU/p16YIrCJfTrLTQZCHfSflLufpvX0mVvP1sns1sk7fu8DP+7Ln16MpeZaWWwMkz0pZf++xgQO4AssmLEsi1QN3J5DFXqUT4JRASRQoBYWkpybQ7lIFssCsASUb0gDBM3x7Bm6NQF3EwKWYuBa4p8nBPbgpB+CeqxoC9pPoBgau99ixR+I6yAPPRhtLwCZ3CbzPkquQVh9Zp3uty8IJLHgpa9OV3ishyyYbOeFPhnZFh0UEeu0SoIC5dm8JoojoRgXqUCq4JcB4GIoKpkMiE2EGDUUFngnoILQiOx+6Bw46IZFYQMJIB1ccL7pn+BH/EtkjsZONgA8TMIeMgAvBF10K1sbmijzuAAAAAElFTkSuQmCC'
|
||||
])
|
||||
.setOriginX(0.5)
|
||||
.setOriginY(4.34)
|
||||
.setIsAnimated(false)
|
||||
.setIsLooping(false)
|
||||
.setFrameWidth(64)
|
||||
.setFrameHeight(22)
|
||||
.setFrameRate(0)
|
||||
.setSprite(hairSprite)
|
||||
.save()
|
||||
|
||||
const characterHair = new CharacterHair()
|
||||
await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setIsSelectable(true).setSprite(hairSprite).save()
|
||||
}
|
||||
|
||||
private async createCharacterEquipment(): Promise<void> {
|
||||
const equipmentSprite = new Sprite()
|
||||
equipmentSprite.id = '5b3932dd-0791-4bb7-bb1e-da9833c3cc50'
|
||||
equipmentSprite.name = 'Male shirt'
|
||||
|
||||
// Create actions similar to createCharacterSprite()
|
||||
// with appropriate sprite data and parameters
|
||||
const actions = [
|
||||
{
|
||||
action: 'idle_right_down',
|
||||
sprites: ['data:image/png;base64,...'],
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
isAnimated: false,
|
||||
isLooping: false,
|
||||
frameWidth: 64,
|
||||
frameHeight: 94,
|
||||
frameRate: 0
|
||||
}
|
||||
// Add other actions...
|
||||
]
|
||||
|
||||
for (const actionData of actions) {
|
||||
const action = new SpriteAction()
|
||||
Object.assign(action, actionData)
|
||||
action.sprite = equipmentSprite
|
||||
await action.save()
|
||||
}
|
||||
|
||||
await equipmentSprite.save()
|
||||
}
|
||||
|
||||
private async createZone(): Promise<void> {
|
||||
const zone = new Zone()
|
||||
await zone
|
||||
.setName('New zone')
|
||||
.setWidth(100)
|
||||
.setHeight(100)
|
||||
.setTiles(Array.from({ length: 100 }, () => Array.from({ length: 100 }, () => 'a2fd8d6f-5042-437a-9c1e-c66b91ecc35b')))
|
||||
.save()
|
||||
|
||||
const effect = new ZoneEffect()
|
||||
await effect.setEffect('light').setStrength(100).setZone(zone).save()
|
||||
}
|
||||
|
||||
private async createUser(): Promise<void> {
|
||||
const user = new User()
|
||||
await user.setId('6f9a58b4-172d-425e-b9ea-71e1d13d81ee').setUsername('root').setEmail('local@host').setPassword('password').setOnline(false).save()
|
||||
|
||||
const character = new Character()
|
||||
await character
|
||||
.setId('26850183-1757-4135-938f-aa1448c49654')
|
||||
.setUser(user)
|
||||
.setName('root')
|
||||
.setRole('gm')
|
||||
.setZone((await ZoneRepository.getFirst())!)
|
||||
.setCharacterType((await CharacterTypeRepository.getFirst()) ?? undefined)
|
||||
.setCharacterHair((await CharacterHairRepository.getFirst()) ?? undefined)
|
||||
.save()
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { Server } from 'socket.io'
|
||||
import ZoneManager from '../managers/zoneManager'
|
||||
|
||||
import { BaseCommand } from '#application/base/baseCommand'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
|
||||
type CommandInput = string[]
|
||||
|
||||
export default class ListZonesCommand {
|
||||
constructor(private readonly io: Server) {}
|
||||
|
||||
export default class ListZonesCommand extends BaseCommand {
|
||||
public execute(input: CommandInput): void {
|
||||
console.log(ZoneManager.getLoadedZones())
|
||||
}
|
||||
|
@ -1,16 +1,14 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import sharp from 'sharp'
|
||||
import { commandLogger } from '../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
import { getPublicPath } from '../utilities/storage'
|
||||
import path from 'path'
|
||||
|
||||
export default class TilesCommand {
|
||||
constructor(private readonly io: Server) {}
|
||||
import { BaseCommand } from '#application/base/baseCommand'
|
||||
import Storage from '#application/storage'
|
||||
|
||||
export default class TilesCommand extends BaseCommand {
|
||||
public async execute(): Promise<void> {
|
||||
// Get all tiles
|
||||
const tilesDir = getPublicPath('tiles')
|
||||
const tilesDir = Storage.getPublicPath('tiles')
|
||||
const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png'))
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
@ -20,14 +18,14 @@ export default class TilesCommand {
|
||||
|
||||
for (const tile of tiles) {
|
||||
// Check if tile is already 66x34
|
||||
const metadata = await sharp(getPublicPath('tiles', tile)).metadata()
|
||||
const metadata = await sharp(Storage.getPublicPath('tiles', tile)).metadata()
|
||||
if (metadata.width === 66 && metadata.height === 34) {
|
||||
commandLogger.info(`Tile ${tile} already processed`)
|
||||
this.logger.info(`Tile ${tile} already processed`)
|
||||
continue
|
||||
}
|
||||
|
||||
const inputPath = getPublicPath('tiles', tile)
|
||||
const tempPath = getPublicPath('tiles', `temp_${tile}`)
|
||||
const inputPath = Storage.getPublicPath('tiles', tile)
|
||||
const tempPath = Storage.getPublicPath('tiles', `temp_${tile}`)
|
||||
|
||||
try {
|
||||
await sharp(inputPath)
|
||||
@ -43,7 +41,7 @@ export default class TilesCommand {
|
||||
fs.unlinkSync(inputPath)
|
||||
fs.renameSync(tempPath, inputPath)
|
||||
|
||||
commandLogger.info(`Processed and replaced: ${tile}`)
|
||||
this.logger.info(`Processed and replaced: ${tile}`)
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${tile}:`, error)
|
||||
// Clean up temp file if it exists
|
||||
@ -53,6 +51,6 @@ export default class TilesCommand {
|
||||
}
|
||||
}
|
||||
|
||||
commandLogger.info('Tile processing completed.')
|
||||
this.logger.info('Tile processing completed.')
|
||||
}
|
||||
}
|
||||
|
297
src/entities/character.ts
Normal file
297
src/entities/character.ts
Normal file
@ -0,0 +1,297 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { CharacterEquipment } from './characterEquipment'
|
||||
import { CharacterHair } from './characterHair'
|
||||
import { CharacterItem } from './characterItem'
|
||||
import { CharacterType } from './characterType'
|
||||
import { Chat } from './chat'
|
||||
import { User } from './user'
|
||||
import { Zone } from './zone'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class Character extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
user!: User
|
||||
|
||||
@Property({ unique: true })
|
||||
name!: string
|
||||
|
||||
@Property()
|
||||
online = false
|
||||
|
||||
@Property()
|
||||
role = 'player'
|
||||
|
||||
@OneToMany(() => Chat, (chat) => chat.character)
|
||||
chats = new Collection<Chat>(this)
|
||||
|
||||
// Position
|
||||
@ManyToOne()
|
||||
zone!: Zone // @TODO: Update to spawn point when current zone is not found
|
||||
|
||||
@Property()
|
||||
positionX = 0
|
||||
|
||||
@Property()
|
||||
positionY = 0
|
||||
|
||||
@Property()
|
||||
rotation = 0
|
||||
|
||||
// Customization
|
||||
@ManyToOne({ deleteRule: 'set null' })
|
||||
characterType?: CharacterType | null | undefined
|
||||
|
||||
@ManyToOne({ deleteRule: 'set null' })
|
||||
characterHair?: CharacterHair | null | undefined
|
||||
|
||||
// Inventory
|
||||
@OneToMany({ mappedBy: 'character' })
|
||||
items = new Collection<CharacterItem>(this)
|
||||
|
||||
@OneToMany({ mappedBy: 'character' })
|
||||
equipment = new Collection<CharacterEquipment>(this)
|
||||
|
||||
// Stats
|
||||
@Property()
|
||||
alignment = 50
|
||||
|
||||
@Property()
|
||||
hitpoints = 100
|
||||
|
||||
@Property()
|
||||
mana = 100
|
||||
|
||||
@Property()
|
||||
level = 1
|
||||
|
||||
@Property()
|
||||
experience = 0
|
||||
|
||||
@Property()
|
||||
strength = 10
|
||||
|
||||
@Property()
|
||||
dexterity = 10
|
||||
|
||||
@Property()
|
||||
intelligence = 10
|
||||
|
||||
@Property()
|
||||
wisdom = 10
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setUser(user: User) {
|
||||
this.user = user
|
||||
return this
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setOnline(online: boolean) {
|
||||
this.online = online
|
||||
return this
|
||||
}
|
||||
|
||||
getOnline() {
|
||||
return this.online
|
||||
}
|
||||
|
||||
setRole(role: string) {
|
||||
this.role = role
|
||||
return this
|
||||
}
|
||||
|
||||
getRole() {
|
||||
return this.role
|
||||
}
|
||||
|
||||
setChats(chats: Collection<Chat>) {
|
||||
this.chats = chats
|
||||
return this
|
||||
}
|
||||
|
||||
getChats() {
|
||||
return this.chats
|
||||
}
|
||||
|
||||
setZone(zone: Zone) {
|
||||
this.zone = zone
|
||||
return this
|
||||
}
|
||||
|
||||
getZone() {
|
||||
return this.zone
|
||||
}
|
||||
|
||||
setPositionX(positionX: number) {
|
||||
this.positionX = positionX
|
||||
return this
|
||||
}
|
||||
|
||||
getPositionX() {
|
||||
return this.positionX
|
||||
}
|
||||
|
||||
setPositionY(positionY: number) {
|
||||
this.positionY = positionY
|
||||
return this
|
||||
}
|
||||
|
||||
getPositionY() {
|
||||
return this.positionY
|
||||
}
|
||||
|
||||
setRotation(rotation: number) {
|
||||
this.rotation = rotation
|
||||
return this
|
||||
}
|
||||
|
||||
getRotation() {
|
||||
return this.rotation
|
||||
}
|
||||
|
||||
setCharacterType(characterType: CharacterType | null | undefined) {
|
||||
this.characterType = characterType
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacterType() {
|
||||
return this.characterType
|
||||
}
|
||||
|
||||
setCharacterHair(characterHair: CharacterHair | null | undefined) {
|
||||
this.characterHair = characterHair
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacterHair() {
|
||||
return this.characterHair
|
||||
}
|
||||
|
||||
setItems(items: Collection<CharacterItem>) {
|
||||
this.items = items
|
||||
return this
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.items
|
||||
}
|
||||
|
||||
setEquipment(equipment: Collection<CharacterEquipment>) {
|
||||
this.equipment = equipment
|
||||
return this
|
||||
}
|
||||
|
||||
getEquipment() {
|
||||
return this.equipment
|
||||
}
|
||||
|
||||
setAlignment(alignment: number) {
|
||||
this.alignment = alignment
|
||||
return this
|
||||
}
|
||||
|
||||
getAlignment() {
|
||||
return this.alignment
|
||||
}
|
||||
|
||||
setHitpoints(hitpoints: number) {
|
||||
this.hitpoints = hitpoints
|
||||
return this
|
||||
}
|
||||
|
||||
getHitpoints() {
|
||||
return this.hitpoints
|
||||
}
|
||||
|
||||
setMana(mana: number) {
|
||||
this.mana = mana
|
||||
return this
|
||||
}
|
||||
|
||||
getMana() {
|
||||
return this.mana
|
||||
}
|
||||
|
||||
setLevel(level: number) {
|
||||
this.level = level
|
||||
return this
|
||||
}
|
||||
|
||||
getLevel() {
|
||||
return this.level
|
||||
}
|
||||
|
||||
setExperience(experience: number) {
|
||||
this.experience = experience
|
||||
return this
|
||||
}
|
||||
|
||||
getExperience() {
|
||||
return this.experience
|
||||
}
|
||||
|
||||
setStrength(strength: number) {
|
||||
this.strength = strength
|
||||
return this
|
||||
}
|
||||
|
||||
getStrength() {
|
||||
return this.strength
|
||||
}
|
||||
|
||||
setDexterity(dexterity: number) {
|
||||
this.dexterity = dexterity
|
||||
return this
|
||||
}
|
||||
|
||||
getDexterity() {
|
||||
return this.dexterity
|
||||
}
|
||||
|
||||
setIntelligence(intelligence: number) {
|
||||
this.intelligence = intelligence
|
||||
return this
|
||||
}
|
||||
|
||||
getIntelligence() {
|
||||
return this.intelligence
|
||||
}
|
||||
|
||||
setWisdom(wisdom: number) {
|
||||
this.wisdom = wisdom
|
||||
return this
|
||||
}
|
||||
|
||||
getWisdom() {
|
||||
return this.wisdom
|
||||
}
|
||||
}
|
61
src/entities/characterEquipment.ts
Normal file
61
src/entities/characterEquipment.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey } from '@mikro-orm/core'
|
||||
|
||||
import { Character } from './character'
|
||||
import { CharacterItem } from './characterItem'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { CharacterEquipmentSlotType } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class CharacterEquipment extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Enum(() => CharacterEquipmentSlotType)
|
||||
slot!: CharacterEquipmentSlotType
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
character!: Character
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
characterItem!: CharacterItem
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setSlot(slot: CharacterEquipmentSlotType) {
|
||||
this.slot = slot
|
||||
return this
|
||||
}
|
||||
|
||||
getSlot() {
|
||||
return this.slot
|
||||
}
|
||||
|
||||
setCharacter(character: Character) {
|
||||
this.character = character
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacter() {
|
||||
return this.character
|
||||
}
|
||||
|
||||
setCharacterItem(characterItem: CharacterItem) {
|
||||
this.characterItem = characterItem
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacterItem() {
|
||||
return this.characterItem
|
||||
}
|
||||
}
|
73
src/entities/characterHair.ts
Normal file
73
src/entities/characterHair.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Character } from './character'
|
||||
import { Sprite } from './sprite'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { CharacterGender } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class CharacterHair extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@Property()
|
||||
gender: CharacterGender = CharacterGender.MALE
|
||||
|
||||
@Property()
|
||||
isSelectable = false
|
||||
|
||||
@ManyToOne({ nullable: true })
|
||||
sprite?: Sprite
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setGender(gender: CharacterGender) {
|
||||
this.gender = gender
|
||||
return this
|
||||
}
|
||||
|
||||
getGender() {
|
||||
return this.gender
|
||||
}
|
||||
|
||||
setIsSelectable(isSelectable: boolean) {
|
||||
this.isSelectable = isSelectable
|
||||
return this
|
||||
}
|
||||
|
||||
getIsSelectable() {
|
||||
return this.isSelectable
|
||||
}
|
||||
|
||||
setSprite(sprite: Sprite) {
|
||||
this.sprite = sprite
|
||||
return this
|
||||
}
|
||||
|
||||
getSprite() {
|
||||
return this.sprite
|
||||
}
|
||||
}
|
61
src/entities/characterItem.ts
Normal file
61
src/entities/characterItem.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Character } from './character'
|
||||
import { CharacterEquipment } from './characterEquipment'
|
||||
import { Item } from './item'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class CharacterItem extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
character!: Character
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
item!: Item
|
||||
|
||||
@Property()
|
||||
quantity!: number
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setCharacter(character: Character) {
|
||||
this.character = character
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacter() {
|
||||
return this.character
|
||||
}
|
||||
|
||||
setItem(item: Item) {
|
||||
this.item = item
|
||||
return this
|
||||
}
|
||||
|
||||
getItem() {
|
||||
return this.item
|
||||
}
|
||||
|
||||
setQuantity(quantity: number) {
|
||||
this.quantity = quantity
|
||||
return this
|
||||
}
|
||||
|
||||
getQuantity() {
|
||||
return this.quantity
|
||||
}
|
||||
}
|
121
src/entities/characterType.ts
Normal file
121
src/entities/characterType.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Character } from './character'
|
||||
import { Sprite } from './sprite'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { CharacterGender, CharacterRace } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class CharacterType extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@Enum(() => CharacterGender)
|
||||
gender!: CharacterGender
|
||||
|
||||
@Enum(() => CharacterRace)
|
||||
race!: CharacterRace
|
||||
|
||||
@Property()
|
||||
isSelectable = false
|
||||
|
||||
@OneToMany(() => Character, (character) => character.characterType)
|
||||
characters = new Collection<Character>(this)
|
||||
|
||||
@ManyToOne(() => Sprite, { nullable: true })
|
||||
sprite?: Sprite
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
@Property()
|
||||
updatedAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setGender(gender: CharacterGender) {
|
||||
this.gender = gender
|
||||
return this
|
||||
}
|
||||
|
||||
getGender() {
|
||||
return this.gender
|
||||
}
|
||||
|
||||
setRace(race: CharacterRace) {
|
||||
this.race = race
|
||||
return this
|
||||
}
|
||||
|
||||
getRace() {
|
||||
return this.race
|
||||
}
|
||||
|
||||
setIsSelectable(isSelectable: boolean) {
|
||||
this.isSelectable = isSelectable
|
||||
return this
|
||||
}
|
||||
|
||||
getIsSelectable() {
|
||||
return this.isSelectable
|
||||
}
|
||||
|
||||
setSprite(sprite: Sprite) {
|
||||
this.sprite = sprite
|
||||
return this
|
||||
}
|
||||
|
||||
getSprite() {
|
||||
return this.sprite
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: Date) {
|
||||
this.updatedAt = updatedAt
|
||||
return this
|
||||
}
|
||||
|
||||
getUpdatedAt() {
|
||||
return this.updatedAt
|
||||
}
|
||||
|
||||
setCharacters(characters: Collection<Character>) {
|
||||
this.characters = characters
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacters() {
|
||||
return this.characters
|
||||
}
|
||||
}
|
72
src/entities/chat.ts
Normal file
72
src/entities/chat.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Character } from './character'
|
||||
import { Zone } from './zone'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class Chat extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
character!: Character
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
zone!: Zone
|
||||
|
||||
@Property()
|
||||
message!: string
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setCharacter(character: Character) {
|
||||
this.character = character
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacter() {
|
||||
return this.character
|
||||
}
|
||||
|
||||
setZone(zone: Zone) {
|
||||
this.zone = zone
|
||||
return this
|
||||
}
|
||||
|
||||
getZone() {
|
||||
return this.zone
|
||||
}
|
||||
|
||||
setMessage(message: string) {
|
||||
this.message = message
|
||||
return this
|
||||
}
|
||||
|
||||
getMessage() {
|
||||
return this.message
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
}
|
121
src/entities/item.ts
Normal file
121
src/entities/item.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { CharacterItem } from './characterItem'
|
||||
import { Sprite } from './sprite'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { ItemType, ItemRarity } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class Item extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
description?: string
|
||||
|
||||
@Enum(() => ItemType)
|
||||
itemType!: ItemType
|
||||
|
||||
@Property()
|
||||
stackable = false
|
||||
|
||||
@Enum(() => ItemRarity)
|
||||
rarity: ItemRarity = ItemRarity.COMMON
|
||||
|
||||
@ManyToOne(() => Sprite, { nullable: true })
|
||||
sprite?: Sprite
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
@Property()
|
||||
updatedAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setDescription(description: string) {
|
||||
this.description = description
|
||||
return this
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this.description
|
||||
}
|
||||
|
||||
setItemType(itemType: ItemType) {
|
||||
this.itemType = itemType
|
||||
return this
|
||||
}
|
||||
|
||||
getItemType() {
|
||||
return this.itemType
|
||||
}
|
||||
|
||||
setStackable(stackable: boolean) {
|
||||
this.stackable = stackable
|
||||
return this
|
||||
}
|
||||
|
||||
getStackable() {
|
||||
return this.stackable
|
||||
}
|
||||
|
||||
setRarity(rarity: ItemRarity) {
|
||||
this.rarity = rarity
|
||||
return this
|
||||
}
|
||||
|
||||
getRarity() {
|
||||
return this.rarity
|
||||
}
|
||||
|
||||
setSprite(sprite: Sprite) {
|
||||
this.sprite = sprite
|
||||
return this
|
||||
}
|
||||
|
||||
getSprite() {
|
||||
return this.sprite
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: Date) {
|
||||
this.updatedAt = updatedAt
|
||||
return this
|
||||
}
|
||||
|
||||
getUpdatedAt() {
|
||||
return this.updatedAt
|
||||
}
|
||||
}
|
143
src/entities/mapObject.ts
Normal file
143
src/entities/mapObject.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { ZoneObject } from './zoneObject'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class MapObject extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
tags?: any
|
||||
|
||||
@Property()
|
||||
originX = 0
|
||||
|
||||
@Property()
|
||||
originY = 0
|
||||
|
||||
@Property()
|
||||
isAnimated = false
|
||||
|
||||
@Property()
|
||||
frameRate = 0
|
||||
|
||||
@Property()
|
||||
frameWidth = 0
|
||||
|
||||
@Property()
|
||||
frameHeight = 0
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
@Property()
|
||||
updatedAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setTags(tags: any) {
|
||||
this.tags = tags
|
||||
return this
|
||||
}
|
||||
|
||||
getTags() {
|
||||
return this.tags
|
||||
}
|
||||
|
||||
setOriginX(originX: number) {
|
||||
this.originX = originX
|
||||
return this
|
||||
}
|
||||
|
||||
getOriginX() {
|
||||
return this.originX
|
||||
}
|
||||
|
||||
setOriginY(originY: number) {
|
||||
this.originY = originY
|
||||
return this
|
||||
}
|
||||
|
||||
getOriginY() {
|
||||
return this.originY
|
||||
}
|
||||
|
||||
setIsAnimated(isAnimated: boolean) {
|
||||
this.isAnimated = isAnimated
|
||||
return this
|
||||
}
|
||||
|
||||
getIsAnimated() {
|
||||
return this.isAnimated
|
||||
}
|
||||
|
||||
setFrameRate(frameRate: number) {
|
||||
this.frameRate = frameRate
|
||||
return this
|
||||
}
|
||||
|
||||
getFrameRate() {
|
||||
return this.frameRate
|
||||
}
|
||||
|
||||
setFrameWidth(frameWidth: number) {
|
||||
this.frameWidth = frameWidth
|
||||
return this
|
||||
}
|
||||
|
||||
getFrameWidth() {
|
||||
return this.frameWidth
|
||||
}
|
||||
|
||||
setFrameHeight(frameHeight: number) {
|
||||
this.frameHeight = frameHeight
|
||||
return this
|
||||
}
|
||||
|
||||
getFrameHeight() {
|
||||
return this.frameHeight
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: Date) {
|
||||
this.updatedAt = updatedAt
|
||||
return this
|
||||
}
|
||||
|
||||
getUpdatedAt() {
|
||||
return this.updatedAt
|
||||
}
|
||||
}
|
59
src/entities/passwordResetToken.ts
Normal file
59
src/entities/passwordResetToken.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { User } from './user'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class PasswordResetToken extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
user!: User
|
||||
|
||||
@Property({ unique: true })
|
||||
token!: string
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setUser(user: User) {
|
||||
this.user = user
|
||||
return this
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token
|
||||
return this
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.token
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
}
|
71
src/entities/sprite.ts
Normal file
71
src/entities/sprite.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { SpriteAction } from './spriteAction'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class Sprite extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@OneToMany(() => SpriteAction, (action) => action.sprite)
|
||||
spriteActions = new Collection<SpriteAction>(this)
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
@Property()
|
||||
updatedAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setSpriteActions(spriteActions: Collection<SpriteAction>) {
|
||||
this.spriteActions = spriteActions
|
||||
return this
|
||||
}
|
||||
|
||||
getSpriteActions() {
|
||||
return this.spriteActions
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: Date) {
|
||||
this.updatedAt = updatedAt
|
||||
return this
|
||||
}
|
||||
|
||||
getUpdatedAt() {
|
||||
return this.updatedAt
|
||||
}
|
||||
}
|
143
src/entities/spriteAction.ts
Normal file
143
src/entities/spriteAction.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Sprite } from './sprite'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class SpriteAction extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
sprite!: Sprite
|
||||
|
||||
@Property()
|
||||
action!: string
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
sprites?: string[]
|
||||
|
||||
@Property()
|
||||
originX = 0
|
||||
|
||||
@Property()
|
||||
originY = 0
|
||||
|
||||
@Property()
|
||||
isAnimated = false
|
||||
|
||||
@Property()
|
||||
isLooping = false
|
||||
|
||||
@Property()
|
||||
frameWidth = 0
|
||||
|
||||
@Property()
|
||||
frameHeight = 0
|
||||
|
||||
@Property()
|
||||
frameRate = 0
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setSprite(sprite: Sprite) {
|
||||
this.sprite = sprite
|
||||
return this
|
||||
}
|
||||
|
||||
getSprite() {
|
||||
return this.sprite
|
||||
}
|
||||
|
||||
setAction(action: string) {
|
||||
this.action = action
|
||||
return this
|
||||
}
|
||||
|
||||
getAction() {
|
||||
return this.action
|
||||
}
|
||||
|
||||
setSprites(sprites: string[]) {
|
||||
this.sprites = sprites
|
||||
return this
|
||||
}
|
||||
|
||||
getSprites() {
|
||||
return this.sprites
|
||||
}
|
||||
|
||||
setOriginX(originX: number) {
|
||||
this.originX = originX
|
||||
return this
|
||||
}
|
||||
|
||||
getOriginX() {
|
||||
return this.originX
|
||||
}
|
||||
|
||||
setOriginY(originY: number) {
|
||||
this.originY = originY
|
||||
return this
|
||||
}
|
||||
|
||||
getOriginY() {
|
||||
return this.originY
|
||||
}
|
||||
|
||||
setIsAnimated(isAnimated: boolean) {
|
||||
this.isAnimated = isAnimated
|
||||
return this
|
||||
}
|
||||
|
||||
getIsAnimated() {
|
||||
return this.isAnimated
|
||||
}
|
||||
|
||||
setIsLooping(isLooping: boolean) {
|
||||
this.isLooping = isLooping
|
||||
return this
|
||||
}
|
||||
|
||||
getIsLooping() {
|
||||
return this.isLooping
|
||||
}
|
||||
|
||||
setFrameWidth(frameWidth: number) {
|
||||
this.frameWidth = frameWidth
|
||||
return this
|
||||
}
|
||||
|
||||
getFrameWidth() {
|
||||
return this.frameWidth
|
||||
}
|
||||
|
||||
setFrameHeight(frameHeight: number) {
|
||||
this.frameHeight = frameHeight
|
||||
return this
|
||||
}
|
||||
|
||||
getFrameHeight() {
|
||||
return this.frameHeight
|
||||
}
|
||||
|
||||
setFrameRate(frameRate: number) {
|
||||
this.frameRate = frameRate
|
||||
return this
|
||||
}
|
||||
|
||||
getFrameRate() {
|
||||
return this.frameRate
|
||||
}
|
||||
}
|
69
src/entities/tile.ts
Normal file
69
src/entities/tile.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class Tile extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
tags?: any
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
@Property()
|
||||
updatedAt = new Date()
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setTags(tags: any) {
|
||||
this.tags = tags
|
||||
return this
|
||||
}
|
||||
|
||||
getTags() {
|
||||
return this.tags
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: Date) {
|
||||
this.updatedAt = updatedAt
|
||||
return this
|
||||
}
|
||||
|
||||
getUpdatedAt() {
|
||||
return this.updatedAt
|
||||
}
|
||||
}
|
98
src/entities/user.ts
Normal file
98
src/entities/user.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
import { Character } from './character'
|
||||
import { PasswordResetToken } from './passwordResetToken'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property({ unique: true })
|
||||
username!: string
|
||||
|
||||
@Property({ unique: true })
|
||||
email!: string
|
||||
|
||||
@Property()
|
||||
password!: string
|
||||
|
||||
@Property()
|
||||
online = false
|
||||
|
||||
@OneToMany(() => Character, (character) => character.user)
|
||||
characters = new Collection<Character>(this)
|
||||
|
||||
@OneToMany(() => PasswordResetToken, (token) => token.user)
|
||||
passwordResetTokens = new Collection<PasswordResetToken>(this)
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setUsername(username: string) {
|
||||
this.username = username
|
||||
return this
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.username
|
||||
}
|
||||
|
||||
setEmail(email: string) {
|
||||
this.email = email
|
||||
return this
|
||||
}
|
||||
|
||||
getEmail() {
|
||||
return this.email
|
||||
}
|
||||
|
||||
setPassword(password: string) {
|
||||
this.password = bcrypt.hashSync(password, 10)
|
||||
return this
|
||||
}
|
||||
|
||||
getPassword() {
|
||||
return this.password
|
||||
}
|
||||
|
||||
setOnline(online: boolean) {
|
||||
this.online = online
|
||||
return this
|
||||
}
|
||||
|
||||
getOnline() {
|
||||
return this.online
|
||||
}
|
||||
|
||||
setCharacters(characters: Collection<Character>) {
|
||||
this.characters = characters
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacters() {
|
||||
return this.characters
|
||||
}
|
||||
|
||||
setPasswordResetTokens(passwordResetTokens: Collection<PasswordResetToken>) {
|
||||
this.passwordResetTokens = passwordResetTokens
|
||||
return this
|
||||
}
|
||||
|
||||
getPasswordResetTokens() {
|
||||
return this.passwordResetTokens
|
||||
return this
|
||||
}
|
||||
}
|
66
src/entities/world.ts
Normal file
66
src/entities/world.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
|
||||
@Entity()
|
||||
export class World extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
date = new Date()
|
||||
|
||||
@Property()
|
||||
isRainEnabled = false
|
||||
|
||||
@Property()
|
||||
rainPercentage = 0
|
||||
|
||||
@Property()
|
||||
isFogEnabled = false
|
||||
|
||||
@Property()
|
||||
fogDensity = 0
|
||||
|
||||
setDate(date: Date) {
|
||||
this.date = date
|
||||
return this
|
||||
}
|
||||
|
||||
getDate() {
|
||||
return this.date
|
||||
}
|
||||
|
||||
setIsRainEnabled(isRainEnabled: boolean) {
|
||||
this.isRainEnabled = isRainEnabled
|
||||
return this
|
||||
}
|
||||
|
||||
getIsRainEnabled() {
|
||||
return this.isRainEnabled
|
||||
}
|
||||
|
||||
setRainPercentage(rainPercentage: number) {
|
||||
this.rainPercentage = rainPercentage
|
||||
return this
|
||||
}
|
||||
|
||||
getRainPercentage() {
|
||||
return this.rainPercentage
|
||||
}
|
||||
|
||||
setIsFogEnabled(isFogEnabled: boolean) {
|
||||
this.isFogEnabled = isFogEnabled
|
||||
return this
|
||||
}
|
||||
|
||||
getIsFogEnabled() {
|
||||
return this.isFogEnabled
|
||||
}
|
||||
|
||||
setFogDensity(fogDensity: number) {
|
||||
this.fogDensity = fogDensity
|
||||
return this
|
||||
}
|
||||
|
||||
getFogDensity() {
|
||||
return this.fogDensity
|
||||
}
|
||||
}
|
184
src/entities/zone.ts
Normal file
184
src/entities/zone.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Character } from './character'
|
||||
import { Chat } from './chat'
|
||||
import { ZoneEffect } from './zoneEffect'
|
||||
import { ZoneEventTile } from './zoneEventTile'
|
||||
import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
|
||||
import { ZoneObject } from './zoneObject'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class Zone extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@Property()
|
||||
name!: string
|
||||
|
||||
@Property()
|
||||
width = 10
|
||||
|
||||
@Property()
|
||||
height = 10
|
||||
|
||||
@Property({ type: 'json', nullable: true })
|
||||
tiles?: any
|
||||
|
||||
@Property()
|
||||
pvp = false
|
||||
|
||||
@Property()
|
||||
createdAt = new Date()
|
||||
|
||||
@Property()
|
||||
updatedAt = new Date()
|
||||
|
||||
@OneToMany(() => ZoneEffect, (effect) => effect.zone)
|
||||
zoneEffects = new Collection<ZoneEffect>(this)
|
||||
|
||||
@OneToMany(() => ZoneEventTile, (tile) => tile.zone)
|
||||
zoneEventTiles = new Collection<ZoneEventTile>(this)
|
||||
|
||||
@OneToMany(() => ZoneEventTileTeleport, (teleport) => teleport.toZone)
|
||||
zoneEventTileTeleports = new Collection<ZoneEventTileTeleport>(this)
|
||||
|
||||
@OneToMany(() => ZoneObject, (object) => object.zone)
|
||||
zoneObjects = new Collection<ZoneObject>(this)
|
||||
|
||||
@OneToMany(() => Character, (character) => character.zone)
|
||||
characters = new Collection<Character>(this)
|
||||
|
||||
@OneToMany(() => Chat, (chat) => chat.zone)
|
||||
chats = new Collection<Chat>(this)
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setWidth(width: number) {
|
||||
this.width = width
|
||||
return this
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
return this.width
|
||||
}
|
||||
|
||||
setHeight(height: number) {
|
||||
this.height = height
|
||||
return this
|
||||
}
|
||||
|
||||
getHeight() {
|
||||
return this.height
|
||||
}
|
||||
|
||||
setTiles(tiles: any) {
|
||||
this.tiles = tiles
|
||||
return this
|
||||
}
|
||||
|
||||
getTiles() {
|
||||
return this.tiles
|
||||
}
|
||||
|
||||
setPvp(pvp: boolean) {
|
||||
this.pvp = pvp
|
||||
return this
|
||||
}
|
||||
|
||||
getPvp() {
|
||||
return this.pvp
|
||||
}
|
||||
|
||||
setCreatedAt(createdAt: Date) {
|
||||
this.createdAt = createdAt
|
||||
return this
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: Date) {
|
||||
this.updatedAt = updatedAt
|
||||
return this
|
||||
}
|
||||
|
||||
getUpdatedAt() {
|
||||
return this.updatedAt
|
||||
}
|
||||
|
||||
setZoneEffects(zoneEffects: Collection<ZoneEffect>) {
|
||||
this.zoneEffects = zoneEffects
|
||||
return this
|
||||
}
|
||||
|
||||
getZoneEffects() {
|
||||
return this.zoneEffects
|
||||
}
|
||||
|
||||
setZoneEventTiles(zoneEventTiles: Collection<ZoneEventTile>) {
|
||||
this.zoneEventTiles = zoneEventTiles
|
||||
return this
|
||||
}
|
||||
|
||||
getZoneEventTiles() {
|
||||
return this.zoneEventTiles
|
||||
}
|
||||
|
||||
setZoneEventTileTeleports(zoneEventTileTeleports: Collection<ZoneEventTileTeleport>) {
|
||||
this.zoneEventTileTeleports = zoneEventTileTeleports
|
||||
return this
|
||||
}
|
||||
|
||||
getZoneEventTileTeleports() {
|
||||
return this.zoneEventTileTeleports
|
||||
}
|
||||
|
||||
setZoneObjects(zoneObjects: Collection<ZoneObject>) {
|
||||
this.zoneObjects = zoneObjects
|
||||
return this
|
||||
}
|
||||
|
||||
getZoneObjects() {
|
||||
return this.zoneObjects
|
||||
}
|
||||
|
||||
setCharacters(characters: Collection<Character>) {
|
||||
this.characters = characters
|
||||
return this
|
||||
}
|
||||
|
||||
getCharacters() {
|
||||
return this.characters
|
||||
}
|
||||
|
||||
setChats(chats: Collection<Chat>) {
|
||||
this.chats = chats
|
||||
return this
|
||||
}
|
||||
|
||||
getChats() {
|
||||
return this.chats
|
||||
}
|
||||
}
|
59
src/entities/zoneEffect.ts
Normal file
59
src/entities/zoneEffect.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Zone } from './zone'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class ZoneEffect extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
zone!: Zone
|
||||
|
||||
@Property()
|
||||
effect!: string
|
||||
|
||||
@Property()
|
||||
strength!: number
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setZone(zone: Zone) {
|
||||
this.zone = zone
|
||||
return this
|
||||
}
|
||||
|
||||
getZone() {
|
||||
return this.zone
|
||||
}
|
||||
|
||||
setEffect(effect: string) {
|
||||
this.effect = effect
|
||||
return this
|
||||
}
|
||||
|
||||
getEffect() {
|
||||
return this.effect
|
||||
}
|
||||
|
||||
setStrength(strength: number) {
|
||||
this.strength = strength
|
||||
return this
|
||||
}
|
||||
|
||||
getStrength() {
|
||||
return this.strength
|
||||
}
|
||||
}
|
85
src/entities/zoneEventTile.ts
Normal file
85
src/entities/zoneEventTile.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Zone } from './zone'
|
||||
import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { ZoneEventTileType } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class ZoneEventTile extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
zone!: Zone
|
||||
|
||||
@Enum(() => ZoneEventTileType)
|
||||
type!: ZoneEventTileType
|
||||
|
||||
@Property()
|
||||
positionX!: number
|
||||
|
||||
@Property()
|
||||
positionY!: number
|
||||
|
||||
@OneToOne(() => ZoneEventTileTeleport, (teleport) => teleport.zoneEventTile)
|
||||
teleport?: ZoneEventTileTeleport
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setZone(zone: Zone) {
|
||||
this.zone = zone
|
||||
return this
|
||||
}
|
||||
|
||||
getZone() {
|
||||
return this.zone
|
||||
}
|
||||
|
||||
setType(type: ZoneEventTileType) {
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type
|
||||
}
|
||||
|
||||
setPositionX(positionX: number) {
|
||||
this.positionX = positionX
|
||||
return this
|
||||
}
|
||||
|
||||
getPositionX() {
|
||||
return this.positionX
|
||||
}
|
||||
|
||||
setPositionY(positionY: number) {
|
||||
this.positionY = positionY
|
||||
return this
|
||||
}
|
||||
|
||||
getPositionY() {
|
||||
return this.positionY
|
||||
}
|
||||
|
||||
setTeleport(teleport: ZoneEventTileTeleport) {
|
||||
this.teleport = teleport
|
||||
return this
|
||||
}
|
||||
|
||||
getTeleport() {
|
||||
return this.teleport
|
||||
}
|
||||
}
|
84
src/entities/zoneEventTileTeleport.ts
Normal file
84
src/entities/zoneEventTileTeleport.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Zone } from './zone'
|
||||
import { ZoneEventTile } from './zoneEventTile'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
|
||||
@Entity()
|
||||
export class ZoneEventTileTeleport extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@OneToOne({ deleteRule: 'cascade' })
|
||||
zoneEventTile!: ZoneEventTile
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
toZone!: Zone
|
||||
|
||||
@Property()
|
||||
toRotation!: number
|
||||
|
||||
@Property()
|
||||
toPositionX!: number
|
||||
|
||||
@Property()
|
||||
toPositionY!: number
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setZoneEventTile(zoneEventTile: ZoneEventTile) {
|
||||
this.zoneEventTile = zoneEventTile
|
||||
return this
|
||||
}
|
||||
|
||||
getZoneEventTile() {
|
||||
return this.zoneEventTile
|
||||
}
|
||||
|
||||
setToZone(toZone: Zone) {
|
||||
this.toZone = toZone
|
||||
return this
|
||||
}
|
||||
|
||||
getToZone() {
|
||||
return this.toZone
|
||||
}
|
||||
|
||||
setToRotation(toRotation: number) {
|
||||
this.toRotation = toRotation
|
||||
return this
|
||||
}
|
||||
|
||||
getToRotation() {
|
||||
return this.toRotation
|
||||
}
|
||||
|
||||
setToPositionX(toPositionX: number) {
|
||||
this.toPositionX = toPositionX
|
||||
return this
|
||||
}
|
||||
|
||||
getToPositionX() {
|
||||
return this.toPositionX
|
||||
}
|
||||
|
||||
setToPositionY(toPositionY: number) {
|
||||
this.toPositionY = toPositionY
|
||||
return this
|
||||
}
|
||||
|
||||
getToPositionY() {
|
||||
return this.toPositionY
|
||||
}
|
||||
}
|
97
src/entities/zoneObject.ts
Normal file
97
src/entities/zoneObject.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
|
||||
import { Zone } from './zone'
|
||||
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
import { UUID } from '#application/types'
|
||||
import { MapObject } from '#entities/mapObject'
|
||||
|
||||
//@TODO : Rename mapObject
|
||||
@Entity()
|
||||
export class ZoneObject extends BaseEntity {
|
||||
@PrimaryKey()
|
||||
id = randomUUID()
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
zone!: Zone
|
||||
|
||||
@ManyToOne({ deleteRule: 'cascade' })
|
||||
mapObject!: MapObject
|
||||
|
||||
@Property()
|
||||
depth = 0
|
||||
|
||||
@Property()
|
||||
isRotated = false
|
||||
|
||||
@Property()
|
||||
positionX = 0
|
||||
|
||||
@Property()
|
||||
positionY = 0
|
||||
|
||||
setId(id: UUID) {
|
||||
this.id = id
|
||||
return this
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id
|
||||
}
|
||||
|
||||
setZone(zone: Zone) {
|
||||
this.zone = zone
|
||||
return this
|
||||
}
|
||||
|
||||
getZone() {
|
||||
return this.zone
|
||||
}
|
||||
|
||||
setMapObject(mapObject: MapObject) {
|
||||
this.mapObject = mapObject
|
||||
return this
|
||||
}
|
||||
|
||||
getMapObject() {
|
||||
return this.mapObject
|
||||
}
|
||||
|
||||
setDepth(depth: number) {
|
||||
this.depth = depth
|
||||
return this
|
||||
}
|
||||
|
||||
getDepth() {
|
||||
return this.depth
|
||||
}
|
||||
|
||||
setIsRotated(isRotated: boolean) {
|
||||
this.isRotated = isRotated
|
||||
return this
|
||||
}
|
||||
|
||||
getIsRotated() {
|
||||
return this.isRotated
|
||||
}
|
||||
|
||||
setPositionX(positionX: number) {
|
||||
this.positionX = positionX
|
||||
return this
|
||||
}
|
||||
|
||||
getPositionX() {
|
||||
return this.positionX
|
||||
}
|
||||
|
||||
setPositionY(positionY: number) {
|
||||
this.positionY = positionY
|
||||
return this
|
||||
}
|
||||
|
||||
getPositionY() {
|
||||
return this.positionY
|
||||
}
|
||||
}
|
18
src/events/character/charactersScreen/characterHairList.ts
Normal file
18
src/events/character/charactersScreen/characterHairList.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import Database from '#application/database'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
import characterHairRepository from '#repositories/characterHairRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class characterHairListEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:hair:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const items: CharacterHair[] = await characterHairRepository.getAllSelectable()
|
||||
await Database.getEntityManager().populate(items, ['sprite'])
|
||||
callback(items)
|
||||
}
|
||||
}
|
80
src/events/character/connect.ts
Normal file
80
src/events/character/connect.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { UUID } from '#application/types'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import TeleportService from '#services/teleportService'
|
||||
|
||||
interface CharacterConnectPayload {
|
||||
characterId: UUID
|
||||
characterHairId?: UUID
|
||||
}
|
||||
|
||||
export default class CharacterConnectEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:connect', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle character connect event
|
||||
* @TODO:
|
||||
* 1. Check if character is already connected
|
||||
* 2. Update character hair if provided
|
||||
* 3. Emit character connect event
|
||||
* 4. Let other clients know of new character
|
||||
* @param data
|
||||
* @param callback
|
||||
* @private
|
||||
*/
|
||||
private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {
|
||||
if (!this.socket.userId) {
|
||||
this.emitError('User not authenticated')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (await this.checkForActiveCharacters()) {
|
||||
this.emitError('You are already connected to another character')
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId, data.characterId)
|
||||
|
||||
if (!character) {
|
||||
this.emitError('Character not found or does not belong to this user')
|
||||
return
|
||||
}
|
||||
|
||||
// Set character id
|
||||
this.socket.characterId = character.id
|
||||
|
||||
// Set character hair
|
||||
if (data.characterHairId !== undefined && data.characterHairId !== null) {
|
||||
const characterHair = await CharacterHairRepository.getById(data.characterHairId)
|
||||
await character.setCharacterHair(characterHair).update()
|
||||
}
|
||||
|
||||
// Emit character connect event
|
||||
callback({ character })
|
||||
|
||||
// wait 300 ms, @TODO: Find a better way to do this
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
await TeleportService.teleportCharacter(character.id, {
|
||||
targetZoneId: character.zone.id,
|
||||
targetX: character.positionX,
|
||||
targetY: character.positionY,
|
||||
rotation: character.rotation,
|
||||
isInitialJoin: true,
|
||||
character
|
||||
})
|
||||
} catch (error) {
|
||||
this.handleError('Failed to connect character', error)
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForActiveCharacters(): Promise<boolean> {
|
||||
const characters = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
return characters?.some((char) => ZoneManager.getCharacterById(char.id)) ?? false
|
||||
}
|
||||
}
|
63
src/events/character/create.ts
Normal file
63
src/events/character/create.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { ZCharacterCreate } from '#application/zodTypes'
|
||||
import { Character } from '#entities/character'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import UserRepository from '#repositories/userRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
export default class CharacterCreateEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: any): Promise<any> {
|
||||
// zod validate
|
||||
try {
|
||||
data = ZCharacterCreate.parse(data)
|
||||
|
||||
const user = await UserRepository.getById(this.socket.userId!)
|
||||
|
||||
if (!user) {
|
||||
return this.socket.emit('notification', { message: 'User not found' })
|
||||
}
|
||||
|
||||
// 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' })
|
||||
}
|
||||
|
||||
let characters: Character[] = await CharacterRepository.getByUserId(user.getId())
|
||||
|
||||
if (characters.length >= 4) {
|
||||
return this.socket.emit('notification', { message: 'You can only have 4 characters' })
|
||||
}
|
||||
|
||||
// @TODO: Change to default location
|
||||
const zone = await ZoneRepository.getFirst()
|
||||
|
||||
const newCharacter = new Character()
|
||||
await newCharacter.setName(data.name).setUser(user).setZone(zone!).save()
|
||||
|
||||
if (!newCharacter) {
|
||||
return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' })
|
||||
}
|
||||
|
||||
characters = [...characters, newCharacter]
|
||||
|
||||
this.socket.emit('character:create:success')
|
||||
this.socket.emit('character:list', characters)
|
||||
|
||||
this.logger.info('character:create success')
|
||||
} catch (error: any) {
|
||||
this.logger.error(`character:create error: ${error.message}`)
|
||||
if (error instanceof ZodError) {
|
||||
return this.socket.emit('notification', { message: error.issues[0].message })
|
||||
}
|
||||
return this.socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
|
||||
}
|
||||
}
|
||||
}
|
35
src/events/character/delete.ts
Normal file
35
src/events/character/delete.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { UUID } from '#application/types'
|
||||
import { Character } from '#entities/character'
|
||||
import { Zone } from '#entities/zone'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
type TypePayload = {
|
||||
characterId: UUID
|
||||
}
|
||||
|
||||
type TypeResponse = {
|
||||
zone: Zone
|
||||
characters: Character[]
|
||||
}
|
||||
|
||||
export default class CharacterDeleteEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:delete', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
|
||||
try {
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId)
|
||||
if (character) {
|
||||
await character.delete()
|
||||
}
|
||||
|
||||
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
||||
}
|
||||
}
|
||||
}
|
21
src/events/character/list.ts
Normal file
21
src/events/character/list.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import Database from '#application/database'
|
||||
import { Character } from '#entities/character'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class CharacterListEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('character:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: any): Promise<void> {
|
||||
try {
|
||||
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
|
||||
await Database.getEntityManager().populate(characters, ['characterType', 'characterHair'])
|
||||
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
this.logger.error('character:list error', error.message)
|
||||
}
|
||||
}
|
||||
}
|
46
src/events/chat/gameMaster/alertCommand.ts
Normal file
46
src/events/chat/gameMaster/alertCommand.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class AlertCommandEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'alert')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const args = ChatService.getArgs('alert', data.message)
|
||||
|
||||
if (!args) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
|
||||
return callback(true)
|
||||
} catch (error: any) {
|
||||
this.logger.error('chat:alert_command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
53
src/events/chat/gameMaster/setTimeCommand.ts
Normal file
53
src/events/chat/gameMaster/setTimeCommand.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import DateManager from '#managers/dateManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class SetTimeCommand extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'time')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get arguments
|
||||
const args = ChatService.getArgs('time', data.message)
|
||||
|
||||
if (!args) {
|
||||
return
|
||||
}
|
||||
|
||||
const time = args[0] // 24h time, e.g. 17:34
|
||||
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
await DateManager.setTime(time)
|
||||
} catch (error: any) {
|
||||
this.logger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
100
src/events/chat/gameMaster/teleportCommand.ts
Normal file
100
src/events/chat/gameMaster/teleportCommand.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { UUID } from '#application/types'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
import TeleportService from '#services/teleportService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class TeleportCommandEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void) {
|
||||
try {
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.error('chat:message error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!ChatService.isCommand(data.message, 'teleport')) return
|
||||
|
||||
const args = ChatService.getArgs('teleport', data.message)
|
||||
|
||||
if (!args || args.length === 0 || args.length > 3) {
|
||||
this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: 'Usage: /teleport <zoneId> [x] [y]'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const zoneId = args[0] as UUID
|
||||
const targetX = args[1] ? parseInt(args[1], 10) : 0
|
||||
const targetY = args[2] ? parseInt(args[2], 10) : 0
|
||||
|
||||
if (!zoneId || isNaN(targetX) || isNaN(targetY)) {
|
||||
this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: 'Invalid parameters. X and Y coordinates must be numbers.'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const zone = await ZoneRepository.getById(zoneId)
|
||||
if (!zone) {
|
||||
this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: 'Zone not found'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (character.zone.id === zone.id && targetX === character.positionX && targetY === character.positionY) {
|
||||
this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: 'You are already at that location'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const success = await TeleportService.teleportCharacter(character.id, {
|
||||
targetZoneId: zone.id,
|
||||
targetX,
|
||||
targetY,
|
||||
rotation: character.rotation
|
||||
})
|
||||
|
||||
if (!success) {
|
||||
return this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: 'Failed to teleport'
|
||||
})
|
||||
}
|
||||
|
||||
this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: `Teleported to ${zone.name} (${targetX}, ${targetY})`
|
||||
})
|
||||
this.logger.info('teleport', `Character ${character.id} teleported to zone ${zone.id} at position (${targetX}, ${targetY})`)
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error in teleport command: ${error.message}`)
|
||||
this.socket.emit('notification', {
|
||||
title: 'Server message',
|
||||
message: 'An error occurred while teleporting'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
40
src/events/chat/gameMaster/toggleFogCommand.ts
Normal file
40
src/events/chat/gameMaster/toggleFogCommand.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ToggleFogCommand extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'fog')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
await WeatherManager.toggleFog()
|
||||
} catch (error: any) {
|
||||
this.logger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
40
src/events/chat/gameMaster/toggleRainCommand.ts
Normal file
40
src/events/chat/gameMaster/toggleRainCommand.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ToggleRainCommand extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!ChatService.isCommand(data.message, 'rain')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if character exists
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
|
||||
if (!character) {
|
||||
this.logger.error('chat:alert_command error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is the GM
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
|
||||
return
|
||||
}
|
||||
|
||||
await WeatherManager.toggleRain()
|
||||
} catch (error: any) {
|
||||
this.logger.error('command error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
45
src/events/chat/message.ts
Normal file
45
src/events/chat/message.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
import ChatService from '#services/chatService'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class ChatMessageEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('chat:message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!data.message || ChatService.isCommand(data.message)) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.error('chat:message error', 'Character not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const character = zoneCharacter.character
|
||||
|
||||
const zone = await ZoneRepository.getById(character.zone.id)
|
||||
if (!zone) {
|
||||
this.logger.error('chat:message error', 'Zone not found')
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
if (await ChatService.sendZoneMessage(character.getId(), zone.getId(), data.message)) {
|
||||
return callback(true)
|
||||
}
|
||||
|
||||
callback(false)
|
||||
} catch (error: any) {
|
||||
this.logger.error('chat:message error', error.message)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
30
src/events/disconnect.ts
Normal file
30
src/events/disconnect.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
|
||||
export default class DisconnectEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('disconnect', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.userId) {
|
||||
this.logger.info('User disconnected but had no user set')
|
||||
return
|
||||
}
|
||||
|
||||
this.io.emit('user:disconnect', this.socket.userId)
|
||||
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.info('User disconnected but had no character set')
|
||||
return
|
||||
}
|
||||
|
||||
await zoneCharacter.disconnect(this.socket, this.io)
|
||||
this.logger.info('User disconnected along with their character')
|
||||
} catch (error: any) {
|
||||
this.logger.error('disconnect error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
28
src/events/gameMaster/assetManager/characterHair/create.ts
Normal file
28
src/events/gameMaster/assetManager/characterHair/create.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class CharacterHairCreateEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newCharacterHair = new CharacterHair()
|
||||
await newCharacterHair.setName('New hair').save()
|
||||
|
||||
callback(true, newCharacterHair)
|
||||
} catch (error) {
|
||||
console.error('Error creating character hair:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
34
src/events/gameMaster/assetManager/characterHair/delete.ts
Normal file
34
src/events/gameMaster/assetManager/characterHair/delete.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default class characterHairDeleteEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const characterHair = await CharacterHairRepository.getById(data.id)
|
||||
if (characterHair) {
|
||||
await characterHair.delete()
|
||||
}
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
29
src/events/gameMaster/assetManager/characterHair/list.ts
Normal file
29
src/events/gameMaster/assetManager/characterHair/list.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
import characterHairRepository from '#repositories/characterHairRepository'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class characterHairListEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
this.logger.error('gm:characterHair:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
this.logger.info(`User ${character.id} tried to list character hair but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all objects
|
||||
const items = await characterHairRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
43
src/events/gameMaster/assetManager/characterHair/update.ts
Normal file
43
src/events/gameMaster/assetManager/characterHair/update.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { CharacterGender } from '#application/enums'
|
||||
import { UUID } from '#application/types'
|
||||
import CharacterHairRepository from '#repositories/characterHairRepository'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import SpriteRepository from '#repositories/spriteRepository'
|
||||
|
||||
type Payload = {
|
||||
id: number
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
isSelectable: boolean
|
||||
spriteId: UUID
|
||||
}
|
||||
|
||||
export default class CharacterHairUpdateEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterHair:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const sprite = await SpriteRepository.getById(data.spriteId)
|
||||
const characterHair = await CharacterHairRepository.getById(data.id)
|
||||
|
||||
if (characterHair) {
|
||||
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update()
|
||||
}
|
||||
|
||||
return callback(true)
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class CharacterTypeCreateEvent {
|
||||
constructor(
|
||||
@ -11,10 +12,10 @@ export default class CharacterTypeCreateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:create', this.handleCharacterTypeCreate.bind(this))
|
||||
this.socket.on('gm:characterType:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterTypeCreate(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
41
src/events/gameMaster/assetManager/characterType/delete.ts
Normal file
41
src/events/gameMaster/assetManager/characterType/delete.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import CharacterTypeRepository from '#repositories/characterTypeRepository'
|
||||
|
||||
interface IPayload {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default class CharacterTypeDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const characterType = await CharacterTypeRepository.getById(data.id)
|
||||
if (!characterType) return callback(false)
|
||||
|
||||
await characterType.delete()
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { CharacterType } from '@prisma/client'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import CharacterTypeRepository from '../../../../repositories/characterTypeRepository'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import CharacterTypeRepository from '#repositories/characterTypeRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
@ -14,10 +15,10 @@ export default class CharacterTypeListEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:list', this.handleCharacterTypeList.bind(this))
|
||||
this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterTypeList(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
|
53
src/events/gameMaster/assetManager/characterType/update.ts
Normal file
53
src/events/gameMaster/assetManager/characterType/update.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: number
|
||||
name: string
|
||||
gender: CharacterGender
|
||||
race: CharacterRace
|
||||
isSelectable: boolean
|
||||
spriteId: string
|
||||
}
|
||||
|
||||
export default class CharacterTypeUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.characterType.update({
|
||||
where: { id: data.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
gender: data.gender,
|
||||
race: data.race,
|
||||
isSelectable: data.isSelectable,
|
||||
spriteId: data.spriteId
|
||||
}
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
42
src/events/gameMaster/assetManager/item/create.ts
Normal file
42
src/events/gameMaster/assetManager/item/create.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class ItemCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newItem = await prisma.item.create({
|
||||
data: {
|
||||
name: 'New Item',
|
||||
itemType: 'WEAPON',
|
||||
stackable: false,
|
||||
rarity: 'COMMON',
|
||||
spriteId: null
|
||||
}
|
||||
})
|
||||
|
||||
callback(true, newItem)
|
||||
} catch (error) {
|
||||
console.error('Error creating item:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
41
src/events/gameMaster/assetManager/item/delete.ts
Normal file
41
src/events/gameMaster/assetManager/item/delete.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class ItemDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.item.delete({
|
||||
where: { id: data.id }
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error deleting item ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
37
src/events/gameMaster/assetManager/item/list.ts
Normal file
37
src/events/gameMaster/assetManager/item/list.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Item } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import itemRepository from '#repositories/itemRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class ItemListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:item:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to list items but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all items
|
||||
const items = await itemRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
56
src/events/gameMaster/assetManager/item/update.ts
Normal file
56
src/events/gameMaster/assetManager/item/update.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { ItemType, ItemRarity } from '@prisma/client'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
itemType: ItemType
|
||||
stackable: boolean
|
||||
rarity: ItemRarity
|
||||
spriteId: string | null
|
||||
}
|
||||
|
||||
export default class ItemUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:item:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.item.update({
|
||||
where: { id: data.id },
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
itemType: data.itemType,
|
||||
stackable: data.stackable,
|
||||
rarity: data.rarity,
|
||||
spriteId: data.spriteId
|
||||
}
|
||||
})
|
||||
|
||||
return callback(true)
|
||||
} catch (error) {
|
||||
gameMasterLogger.error(`Error updating item: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { Object } from '@prisma/client'
|
||||
import ObjectRepository from '../../../../repositories/objectRepository'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import ObjectRepository from '#repositories/objectRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
@ -13,10 +14,10 @@ export default class ObjectListEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:list', this.handleObjectList.bind(this))
|
||||
this.socket.on('gm:object:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectList(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback([])
|
||||
|
@ -1,9 +1,12 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
import { gameLogger, gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IPayload {
|
||||
object: string
|
||||
@ -16,10 +19,10 @@ export default class ObjectRemoveEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:remove', this.handleObjectRemove.bind(this))
|
||||
this.socket.on('gm:object:remove', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectRemove(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
@ -35,21 +38,21 @@ export default class ObjectRemoveEvent {
|
||||
})
|
||||
|
||||
// get root path
|
||||
const public_folder = getPublicPath('objects')
|
||||
const public_folder = Storage.getPublicPath('objects')
|
||||
|
||||
// remove the tile from the disk
|
||||
const finalFilePath = getPublicPath('objects', data.object + '.png')
|
||||
const finalFilePath = Storage.getPublicPath('objects', data.object + '.png')
|
||||
fs.unlink(finalFilePath, (err) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
callback(true)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} catch (error) {
|
||||
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -10,7 +11,7 @@ type Payload = {
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameSpeed: number
|
||||
frameRate: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
}
|
||||
@ -22,10 +23,10 @@ export default class ObjectUpdateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:update', this.handleObjectUpdate.bind(this))
|
||||
this.socket.on('gm:object:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
@ -44,7 +45,7 @@ export default class ObjectUpdateEvent {
|
||||
originX: data.originX,
|
||||
originY: data.originY,
|
||||
isAnimated: data.isAnimated,
|
||||
frameSpeed: data.frameSpeed,
|
||||
frameRate: data.frameRate,
|
||||
frameWidth: data.frameWidth,
|
||||
frameHeight: data.frameHeight
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import fs from 'fs/promises'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
|
||||
import sharp from 'sharp'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface IObjectData {
|
||||
[key: string]: Buffer
|
||||
@ -19,10 +21,10 @@ export default class ObjectUploadEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:upload', this.handleObjectUpload.bind(this))
|
||||
this.socket.on('gm:object:upload', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectUpload(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
@ -30,7 +32,7 @@ export default class ObjectUploadEvent {
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
const public_folder = getPublicPath('objects')
|
||||
const public_folder = Storage.getPublicPath('objects')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
@ -54,7 +56,7 @@ export default class ObjectUploadEvent {
|
||||
|
||||
const uuid = object.id
|
||||
const filename = `${uuid}.png`
|
||||
const finalFilePath = getPublicPath('objects', filename)
|
||||
const finalFilePath = Storage.getPublicPath('objects', filename)
|
||||
await writeFile(finalFilePath, objectData)
|
||||
|
||||
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)
|
79
src/events/gameMaster/assetManager/sprite/copy.ts
Normal file
79
src/events/gameMaster/assetManager/sprite/copy.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface CopyPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default class SpriteCopyEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:copy', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!(await this.validateGameMasterAccess())) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const sourceSprite = await prisma.sprite.findUnique({
|
||||
where: { id: payload.id },
|
||||
include: {
|
||||
spriteActions: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!sourceSprite) {
|
||||
throw new Error('Source sprite not found')
|
||||
}
|
||||
|
||||
const newSprite = await prisma.sprite.create({
|
||||
data: {
|
||||
name: `${sourceSprite.name} (Copy)`,
|
||||
spriteActions: {
|
||||
create: sourceSprite.spriteActions.map((action) => ({
|
||||
action: action.action,
|
||||
sprites: action.sprites as Prisma.InputJsonValue,
|
||||
originX: action.originX,
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight,
|
||||
frameRate: action.frameRate
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
this.handleError(error, payload.id, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private async validateGameMasterAccess(): Promise<boolean> {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId!)
|
||||
return character?.role === 'gm'
|
||||
}
|
||||
|
||||
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
|
||||
gameMasterLogger.error(`Error copying sprite ${spriteId}: ${this.getErrorMessage(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
|
||||
private getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import fs from 'fs/promises'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
export default class SpriteCreateEvent {
|
||||
constructor(
|
||||
@ -12,19 +14,19 @@ export default class SpriteCreateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:create', this.handleSpriteCreate.bind(this))
|
||||
this.socket.on('gm:sprite:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteCreate(data: undefined, callback: (response: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const public_folder = getPublicPath('sprites')
|
||||
const public_folder = Storage.getPublicPath('sprites')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
@ -37,7 +39,7 @@ export default class SpriteCreateEvent {
|
||||
const uuid = sprite.id
|
||||
|
||||
// Create folder with uuid
|
||||
const sprite_folder = getPublicPath('sprites', uuid)
|
||||
const sprite_folder = Storage.getPublicPath('sprites', uuid)
|
||||
await fs.mkdir(sprite_folder, { recursive: true })
|
||||
|
||||
callback(true)
|
@ -1,10 +1,12 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import fs from 'fs'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -17,15 +19,15 @@ export default class GMSpriteDeleteEvent {
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.public_folder = getPublicPath('sprites')
|
||||
this.public_folder = Storage.getPublicPath('sprites')
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:delete', this.handleSpriteDelete.bind(this))
|
||||
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId!)
|
||||
if (character?.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
@ -43,7 +45,7 @@ export default class GMSpriteDeleteEvent {
|
||||
}
|
||||
|
||||
private async deleteSpriteFolder(spriteId: string): Promise<void> {
|
||||
const finalFilePath = getPublicPath('sprites', spriteId)
|
||||
const finalFilePath = Storage.getPublicPath('sprites', spriteId)
|
||||
|
||||
if (fs.existsSync(finalFilePath)) {
|
||||
await fs.promises.rmdir(finalFilePath, { recursive: true })
|
@ -1,8 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { Sprite } from '@prisma/client'
|
||||
import SpriteRepository from '../../../../repositories/spriteRepository'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import SpriteRepository from '#repositories/spriteRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
@ -13,11 +14,11 @@ export default class SpriteListEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:list', this.handleSpriteList.bind(this))
|
||||
this.socket.on('gm:sprite:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteList(data: any, callback: (response: Sprite[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
private async handleEvent(data: any, callback: (response: Sprite[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId!)
|
||||
if (!character) return callback([])
|
||||
|
||||
if (character.role !== 'gm') {
|
402
src/events/gameMaster/assetManager/sprite/update.ts
Normal file
402
src/events/gameMaster/assetManager/sprite/update.ts
Normal file
@ -0,0 +1,402 @@
|
||||
import { writeFile, mkdir } from 'node:fs/promises'
|
||||
|
||||
import sharp from 'sharp'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import type { Prisma, SpriteAction } from '@prisma/client'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
|
||||
// Constants
|
||||
const ISOMETRIC_CONFIG = {
|
||||
tileWidth: 64,
|
||||
tileHeight: 32,
|
||||
centerOffset: 32,
|
||||
bodyRatios: {
|
||||
topStart: 0.15,
|
||||
topEnd: 0.45,
|
||||
weightUpper: 0.7,
|
||||
weightLower: 0.3
|
||||
}
|
||||
} as const
|
||||
|
||||
// Types
|
||||
interface ContentBounds {
|
||||
left: number
|
||||
right: number
|
||||
top: number
|
||||
bottom: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> {
|
||||
sprites: string[]
|
||||
}
|
||||
|
||||
interface UpdatePayload {
|
||||
id: string
|
||||
name: string
|
||||
spriteActions: Prisma.JsonValue
|
||||
}
|
||||
|
||||
interface ProcessedSpriteAction extends SpriteActionInput {
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
buffersWithDimensions: ProcessedFrame[]
|
||||
}
|
||||
|
||||
interface ProcessedFrame {
|
||||
buffer: Buffer
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface SpriteAnalysis {
|
||||
massCenter: number
|
||||
spinePosition: number
|
||||
contentBounds: ContentBounds
|
||||
}
|
||||
|
||||
export default class SpriteUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
|
||||
try {
|
||||
if (!(await this.validateGameMasterAccess())) {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const parsedActions = this.validateSpriteActions(payload.spriteActions)
|
||||
|
||||
// Process sprites
|
||||
const processedActions = await Promise.all(
|
||||
parsedActions.map(async (action) => {
|
||||
const spriteBuffers = await this.convertBase64ToBuffers(action.sprites)
|
||||
const frameWidth = ISOMETRIC_CONFIG.tileWidth
|
||||
const frameHeight = await this.calculateOptimalHeight(spriteBuffers)
|
||||
const processedFrames = await this.normalizeFrames(spriteBuffers, frameWidth, frameHeight)
|
||||
|
||||
return {
|
||||
...action,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
buffersWithDimensions: processedFrames
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
this.updateDatabase(payload.id, payload.name, processedActions),
|
||||
this.saveSpritesToDisk(
|
||||
payload.id,
|
||||
processedActions.filter((a) => a.buffersWithDimensions.length > 0)
|
||||
)
|
||||
])
|
||||
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
this.handleError(error, payload.id, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private async validateGameMasterAccess(): Promise<boolean> {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId!)
|
||||
return character?.role === 'gm'
|
||||
}
|
||||
|
||||
private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] {
|
||||
try {
|
||||
const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[]
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('Sprite actions must be an array')
|
||||
}
|
||||
return parsed
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid sprite actions format: ${this.getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async convertBase64ToBuffers(sprites: string[]): Promise<Buffer[]> {
|
||||
return sprites.map((sprite) => Buffer.from(sprite.split(',')[1], 'base64'))
|
||||
}
|
||||
|
||||
private async normalizeFrames(buffers: Buffer[], frameWidth: number, frameHeight: number): Promise<ProcessedFrame[]> {
|
||||
return Promise.all(
|
||||
buffers.map(async (buffer) => {
|
||||
const normalizedBuffer = await this.normalizeIsometricSprite(buffer, frameWidth, frameHeight)
|
||||
return {
|
||||
buffer: normalizedBuffer,
|
||||
width: frameWidth,
|
||||
height: frameHeight
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async calculateOptimalHeight(buffers: Buffer[]): Promise<number> {
|
||||
if (!buffers.length) return ISOMETRIC_CONFIG.tileHeight // Return default height if no buffers
|
||||
|
||||
const heights = await Promise.all(
|
||||
buffers.map(async (buffer) => {
|
||||
const bounds = await this.findContentBounds(buffer)
|
||||
return bounds.height
|
||||
})
|
||||
)
|
||||
return Math.ceil(Math.max(...heights) / 2) * 2
|
||||
}
|
||||
|
||||
private async normalizeIsometricSprite(buffer: Buffer, frameWidth: number, frameHeight: number): Promise<Buffer> {
|
||||
const analysis = await this.analyzeIsometricSprite(buffer)
|
||||
const idealCenter = Math.floor(frameWidth / 2)
|
||||
const offset = Math.round(idealCenter - analysis.massCenter)
|
||||
|
||||
// Process the input sprite
|
||||
const processedInput = await sharp(buffer)
|
||||
.ensureAlpha()
|
||||
.resize({
|
||||
width: frameWidth, // Set maximum width
|
||||
height: frameHeight, // Set maximum height
|
||||
fit: 'inside', // Ensure image fits within dimensions
|
||||
kernel: sharp.kernel.nearest,
|
||||
position: 'center',
|
||||
withoutEnlargement: true // Don't enlarge smaller images
|
||||
})
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
// Create the final composition
|
||||
return sharp({
|
||||
create: {
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.composite([
|
||||
{
|
||||
input: processedInput,
|
||||
left: offset,
|
||||
top: 0,
|
||||
blend: 'over'
|
||||
}
|
||||
])
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256
|
||||
})
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async analyzeIsometricSprite(buffer: Buffer): Promise<SpriteAnalysis> {
|
||||
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
|
||||
const { width, height } = info
|
||||
const upperStart = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topStart)
|
||||
const upperEnd = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topEnd)
|
||||
|
||||
const { columnDensity, upperBodyDensity, bounds } = this.calculatePixelDistribution(data, width, height, upperStart, upperEnd)
|
||||
const spinePosition = this.findSpinePosition(upperBodyDensity)
|
||||
const massCenter = this.calculateWeightedMassCenter(columnDensity, upperBodyDensity)
|
||||
|
||||
return {
|
||||
massCenter,
|
||||
spinePosition,
|
||||
contentBounds: bounds
|
||||
}
|
||||
}
|
||||
|
||||
private calculatePixelDistribution(data: Buffer, width: number, height: number, upperStart: number, upperEnd: number) {
|
||||
const columnDensity = new Array(width).fill(0)
|
||||
const upperBodyDensity = new Array(width).fill(0)
|
||||
const bounds = { left: width, right: 0, top: height, bottom: 0 }
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (data[(y * width + x) * 4 + 3] > 0) {
|
||||
columnDensity[x]++
|
||||
if (y >= upperStart && y <= upperEnd) {
|
||||
upperBodyDensity[x]++
|
||||
}
|
||||
this.updateBounds(bounds, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
columnDensity,
|
||||
upperBodyDensity,
|
||||
bounds: {
|
||||
...bounds,
|
||||
width: bounds.right - bounds.left + 1,
|
||||
height: bounds.bottom - bounds.top + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateBounds(bounds: { left: number; right: number; top: number; bottom: number }, x: number, y: number): void {
|
||||
bounds.left = Math.min(bounds.left, x)
|
||||
bounds.right = Math.max(bounds.right, x)
|
||||
bounds.top = Math.min(bounds.top, y)
|
||||
bounds.bottom = Math.max(bounds.bottom, y)
|
||||
}
|
||||
|
||||
private findSpinePosition(density: number[]): number {
|
||||
return density.reduce((maxIdx, curr, idx, arr) => (curr > arr[maxIdx] ? idx : maxIdx), 0)
|
||||
}
|
||||
|
||||
private calculateWeightedMassCenter(columnDensity: number[], upperBodyDensity: number[]): number {
|
||||
const upperMassCenter = this.calculateMassCenter(upperBodyDensity)
|
||||
const lowerMassCenter = this.calculateMassCenter(columnDensity)
|
||||
|
||||
return Math.round(upperMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightUpper + lowerMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightLower)
|
||||
}
|
||||
|
||||
private calculateMassCenter(density: number[]): number {
|
||||
const totalMass = density.reduce((sum, mass) => sum + mass, 0)
|
||||
if (!totalMass) return 0
|
||||
|
||||
const weightedSum = density.reduce((sum, mass, position) => sum + position * mass, 0)
|
||||
return Math.round(weightedSum / totalMass)
|
||||
}
|
||||
|
||||
private async findContentBounds(buffer: Buffer) {
|
||||
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
|
||||
|
||||
const width = info.width
|
||||
const height = info.height
|
||||
|
||||
let left = width
|
||||
let right = 0
|
||||
let top = height
|
||||
let bottom = 0
|
||||
|
||||
// Find actual content boundaries by checking alpha channel
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = (y * width + x) * 4
|
||||
if (data[idx + 3] > 0) {
|
||||
// If pixel is not transparent
|
||||
left = Math.min(left, x)
|
||||
right = Math.max(right, x)
|
||||
top = Math.min(top, y)
|
||||
bottom = Math.max(bottom, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: right - left + 1,
|
||||
height: bottom - top + 1,
|
||||
leftOffset: left,
|
||||
topOffset: top
|
||||
}
|
||||
}
|
||||
|
||||
private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> {
|
||||
const publicFolder = Storage.getPublicPath('sprites', id)
|
||||
await mkdir(publicFolder, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
actions.map(async (action) => {
|
||||
const spritesheet = await this.createSpritesheet(action.buffersWithDimensions)
|
||||
await writeFile(Storage.getPublicPath('sprites', id, `${action.action}.png`), spritesheet)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async createSpritesheet(frames: ProcessedFrame[]): Promise<Buffer> {
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width: ISOMETRIC_CONFIG.tileWidth * frames.length,
|
||||
height: frames[0].height,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
})
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256,
|
||||
dither: 0
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
return sharp(background)
|
||||
.composite(
|
||||
frames.map((frame, index) => ({
|
||||
input: frame.buffer,
|
||||
left: index * ISOMETRIC_CONFIG.tileWidth,
|
||||
top: 0,
|
||||
blend: 'over'
|
||||
}))
|
||||
)
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: false,
|
||||
palette: true,
|
||||
quality: 100,
|
||||
colors: 256,
|
||||
dither: 0
|
||||
})
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
private async updateDatabase(id: string, name: string, actions: ProcessedSpriteAction[]): Promise<void> {
|
||||
await prisma.sprite.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
spriteActions: {
|
||||
deleteMany: { spriteId: id },
|
||||
create: actions.map(this.mapActionToDatabase)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private mapActionToDatabase(action: ProcessedSpriteAction) {
|
||||
return {
|
||||
action: action.action,
|
||||
sprites: action.sprites,
|
||||
originX: action.originX,
|
||||
originY: action.originY,
|
||||
isAnimated: action.isAnimated,
|
||||
isLooping: action.isLooping,
|
||||
frameWidth: action.frameWidth,
|
||||
frameHeight: action.frameHeight,
|
||||
frameRate: action.frameRate
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
|
||||
gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
|
||||
callback(false)
|
||||
}
|
||||
|
||||
private getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import fs from 'fs/promises'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -17,14 +19,14 @@ export default class GMTileDeleteEvent {
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.public_folder = getPublicPath('tiles')
|
||||
this.public_folder = Storage.getPublicPath('tiles')
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:delete', this.handleTileDelete.bind(this))
|
||||
this.socket.on('gm:tile:delete', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
@ -54,7 +56,7 @@ export default class GMTileDeleteEvent {
|
||||
}
|
||||
|
||||
private async deleteTileFile(tileId: string): Promise<void> {
|
||||
const finalFilePath = getPublicPath('tiles', `${tileId}.png`)
|
||||
const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`)
|
||||
try {
|
||||
await fs.unlink(finalFilePath)
|
||||
} catch (error: any) {
|
@ -1,8 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { Tile } from '@prisma/client'
|
||||
import TileRepository from '../../../../repositories/tileRepository'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
import TileRepository from '#repositories/tileRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
@ -13,10 +14,10 @@ export default class TileListEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:list', this.handleTileList.bind(this))
|
||||
this.socket.on('gm:tile:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileList(data: any, callback: (response: Tile[]) => void): Promise<void> {
|
||||
private async handleEvent(data: any, callback: (response: Tile[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -16,10 +17,10 @@ export default class TileUpdateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:update', this.handleTileUpdate.bind(this))
|
||||
this.socket.on('gm:tile:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import fs from 'fs/promises'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import Storage from '#application/storage'
|
||||
import { TSocket } from '#application/types'
|
||||
import characterRepository from '#repositories/characterRepository'
|
||||
|
||||
interface ITileData {
|
||||
[key: string]: Buffer
|
||||
@ -18,10 +20,10 @@ export default class TileUploadEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:upload', this.handleTileUpload.bind(this))
|
||||
this.socket.on('gm:tile:upload', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileUpload(data: ITileData, callback: (response: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: ITileData, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
@ -30,7 +32,7 @@ export default class TileUploadEvent {
|
||||
return
|
||||
}
|
||||
|
||||
const public_folder = getPublicPath('tiles')
|
||||
const public_folder = Storage.getPublicPath('tiles')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
@ -43,7 +45,7 @@ export default class TileUploadEvent {
|
||||
})
|
||||
const uuid = tile.id
|
||||
const filename = `${uuid}.png`
|
||||
const finalFilePath = getPublicPath('tiles', filename)
|
||||
const finalFilePath = Storage.getPublicPath('tiles', filename)
|
||||
await writeFile(finalFilePath, tileData)
|
||||
})
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import { Zone } from '@prisma/client'
|
||||
import prisma from '../../../utilities/prisma'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
type Payload = {
|
||||
name: string
|
||||
@ -19,10 +20,10 @@ export default class ZoneCreateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:zone_editor:zone:create', this.handleZoneCreate.bind(this))
|
||||
this.socket.on('gm:zone_editor:zone:create', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleZoneCreate(data: Payload, callback: (response: Zone[]) => void): Promise<void> {
|
||||
private async handleEvent(data: Payload, callback: (response: Zone[]) => void): Promise<void> {
|
||||
try {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
@ -1,9 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import prisma from '../../../utilities/prisma'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../utilities/logger'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
type Payload = {
|
||||
zoneId: number
|
||||
@ -16,10 +17,10 @@ export default class ZoneDeleteEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:zone_editor:zone:delete', this.handleZoneDelete.bind(this))
|
||||
this.socket.on('gm:zone_editor:zone:delete', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleZoneDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
@ -1,9 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import { Zone } from '@prisma/client'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
@ -14,10 +15,10 @@ export default class ZoneListEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:zone_editor:zone:list', this.handleZoneList.bind(this))
|
||||
this.socket.on('gm:zone_editor:zone:list', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleZoneList(data: IPayload, callback: (response: Zone[]) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: Zone[]) => void): Promise<void> {
|
||||
try {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
@ -1,9 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import { Zone } from '@prisma/client'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import { TSocket } from '#application/types'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
interface IPayload {
|
||||
zoneId: number
|
||||
@ -16,10 +17,10 @@ export default class ZoneRequestEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:zone_editor:zone:request', this.handleZoneRequest.bind(this))
|
||||
this.socket.on('gm:zone_editor:zone:request', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleZoneRequest(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||
try {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
@ -1,11 +1,12 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import { Zone, ZoneEffect, ZoneEventTileType, ZoneObject } from '@prisma/client'
|
||||
import prisma from '../../../utilities/prisma'
|
||||
import zoneManager from '../../../managers/zoneManager'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { gameMasterLogger } from '#application/logger'
|
||||
import prisma from '#application/prisma'
|
||||
import { TSocket } from '#application/types'
|
||||
import zoneManager from '#managers/zoneManager'
|
||||
import CharacterRepository from '#repositories/characterRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
interface IPayload {
|
||||
zoneId: number
|
||||
@ -39,40 +40,52 @@ export default class ZoneUpdateEvent {
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:zone_editor:zone:update', this.handleZoneUpdate.bind(this))
|
||||
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleZoneUpdate(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
|
||||
try {
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`)
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
|
||||
|
||||
if (!data.zoneId) {
|
||||
gameMasterLogger.info(`User ${character.id} tried to update zone but did not provide a zone id.`)
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
let zone = await ZoneRepository.getById(data.zoneId)
|
||||
|
||||
if (!zone) {
|
||||
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist.`)
|
||||
callback(null)
|
||||
return
|
||||
return callback(null)
|
||||
}
|
||||
|
||||
// If tiles are larger than the zone, remove the extra tiles
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// If zone event tiles are placed outside the zone's bounds, remove these
|
||||
data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
|
||||
|
||||
// If zone objects are placed outside the zone's bounds, remove these
|
||||
data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
|
||||
|
||||
await prisma.zone.update({
|
||||
where: { id: data.zoneId },
|
||||
data: {
|
||||
@ -134,10 +147,13 @@ export default class ZoneUpdateEvent {
|
||||
|
||||
callback(zone)
|
||||
|
||||
/**
|
||||
* @TODO #246: Reload zone for players who are currently in the zone
|
||||
*/
|
||||
zoneManager.unloadZone(data.zoneId)
|
||||
await zoneManager.loadZone(zone)
|
||||
} catch (error: any) {
|
||||
gameMasterLogger.error('gm:zone_editor:zone:update error', error.message)
|
||||
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
|
||||
callback(null)
|
||||
}
|
||||
}
|
22
src/events/login.ts
Normal file
22
src/events/login.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import UserRepository from '#repositories/userRepository'
|
||||
|
||||
export default class LoginEvent extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('login', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private handleEvent(): void {
|
||||
try {
|
||||
if (!this.socket.userId) {
|
||||
this.logger.warn('Login attempt without user data')
|
||||
return
|
||||
}
|
||||
|
||||
this.socket.emit('logged_in', { user: UserRepository.getById(this.socket.userId) })
|
||||
this.logger.info(`User logged in: ${this.socket.userId}`)
|
||||
} catch (error: any) {
|
||||
this.logger.error('login error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
104
src/events/zone/characterMove.ts
Normal file
104
src/events/zone/characterMove.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import { ZoneEventTileWithTeleport } from '#application/types'
|
||||
import ZoneManager from '#managers/zoneManager'
|
||||
import ZoneCharacter from '#models/zoneCharacter'
|
||||
import zoneEventTileRepository from '#repositories/zoneEventTileRepository'
|
||||
import CharacterService from '#services/characterService'
|
||||
import ZoneEventTileService from '#services/zoneEventTileService'
|
||||
|
||||
export default class CharacterMove extends BaseEvent {
|
||||
private readonly characterService = CharacterService
|
||||
private readonly zoneEventTileService = ZoneEventTileService
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('zone:character:move', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter?.character) {
|
||||
this.logger.error('zone:character:move error: Character not found or not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
// If already moving, cancel current movement and wait for it to fully stop
|
||||
if (zoneCharacter.isMoving) {
|
||||
zoneCharacter.isMoving = false
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
const path = await this.characterService.calculatePath(zoneCharacter.character, positionX, positionY)
|
||||
if (!path) {
|
||||
this.io.in(zoneCharacter.character.zone.id).emit('zone:character:moveError', 'No valid path found')
|
||||
return
|
||||
}
|
||||
|
||||
// Start new movement
|
||||
zoneCharacter.isMoving = true
|
||||
zoneCharacter.currentPath = path // Add this property to ZoneCharacter class
|
||||
await this.moveAlongPath(zoneCharacter, path)
|
||||
}
|
||||
|
||||
private async moveAlongPath(zoneCharacter: ZoneCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
|
||||
const { character } = zoneCharacter
|
||||
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
if (!zoneCharacter.isMoving || zoneCharacter.currentPath !== path) {
|
||||
return
|
||||
}
|
||||
|
||||
const [start, end] = [path[i], path[i + 1]]
|
||||
character.rotation = CharacterService.calculateRotation(start.x, start.y, end.x, end.y)
|
||||
|
||||
const zoneEventTile = await zoneEventTileRepository.getEventTileByZoneIdAndPosition(character.zone.id, Math.floor(end.x), Math.floor(end.y))
|
||||
|
||||
if (zoneEventTile?.type === 'BLOCK') break
|
||||
if (zoneEventTile?.type === 'TELEPORT' && zoneEventTile.teleport) {
|
||||
await this.handleZoneEventTile(zoneEventTile as ZoneEventTileWithTeleport)
|
||||
break
|
||||
}
|
||||
|
||||
// Update position first
|
||||
character.positionX = end.x
|
||||
character.positionY = end.y
|
||||
|
||||
// Then emit with the same properties
|
||||
this.io.in(character.zone.id).emit('zone:character:move', {
|
||||
characterId: character.id,
|
||||
positionX: character.positionX,
|
||||
positionY: character.positionY,
|
||||
rotation: character.rotation,
|
||||
isMoving: true
|
||||
})
|
||||
|
||||
await this.characterService.applyMovementDelay()
|
||||
}
|
||||
|
||||
if (zoneCharacter.isMoving && zoneCharacter.currentPath === path) {
|
||||
this.finalizeMovement(zoneCharacter)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleZoneEventTile(zoneEventTile: ZoneEventTileWithTeleport): Promise<void> {
|
||||
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
|
||||
if (!zoneCharacter) {
|
||||
this.logger.error('zone:character:move error: Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (zoneEventTile.teleport) {
|
||||
await this.zoneEventTileService.handleTeleport(this.io, this.socket, zoneCharacter.character, zoneEventTile.teleport)
|
||||
}
|
||||
}
|
||||
|
||||
private finalizeMovement(zoneCharacter: ZoneCharacter): void {
|
||||
zoneCharacter.isMoving = false
|
||||
this.io.in(zoneCharacter.character.zone.id).emit('zone:character:move', {
|
||||
characterId: zoneCharacter.character.id,
|
||||
positionX: zoneCharacter.character.positionX,
|
||||
positionY: zoneCharacter.character.positionY,
|
||||
rotation: zoneCharacter.character.rotation,
|
||||
isMoving: false
|
||||
})
|
||||
}
|
||||
}
|
17
src/events/zone/weather.ts
Normal file
17
src/events/zone/weather.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { BaseEvent } from '#application/base/baseEvent'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
|
||||
export default class Weather extends BaseEvent {
|
||||
public listen(): void {
|
||||
this.socket.on('weather', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
private async handleEvent(): Promise<void> {
|
||||
try {
|
||||
const weather = WeatherManager.getWeatherState()
|
||||
this.socket.emit('weather', weather)
|
||||
} catch (error: any) {
|
||||
this.logger.error('weather error: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
115
src/http/controllers/assets.ts
Normal file
115
src/http/controllers/assets.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { BaseController } from '#application/base/baseController'
|
||||
import Database from '#application/database'
|
||||
import Storage from '#application/storage'
|
||||
import { AssetData, UUID } from '#application/types'
|
||||
import SpriteRepository from '#repositories/spriteRepository'
|
||||
import TileRepository from '#repositories/tileRepository'
|
||||
import ZoneRepository from '#repositories/zoneRepository'
|
||||
|
||||
export class AssetsController extends BaseController {
|
||||
/**
|
||||
* List tiles
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async listTiles(req: Request, res: Response) {
|
||||
const assets: AssetData[] = []
|
||||
const tiles = await TileRepository.getAll()
|
||||
|
||||
for (const tile of tiles) {
|
||||
assets.push({ key: tile.getId(), data: '/assets/tiles/' + tile.getId() + '.png', group: 'tiles', updatedAt: tile.getUpdatedAt() } as AssetData)
|
||||
}
|
||||
|
||||
return this.sendSuccess(res, assets)
|
||||
}
|
||||
|
||||
/**
|
||||
* List tiles by zone
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async listTilesByZone(req: Request, res: Response) {
|
||||
const zoneId = req.params.zoneId as UUID
|
||||
|
||||
if (!zoneId) {
|
||||
return this.sendError(res, 'Invalid zone ID', 400)
|
||||
}
|
||||
|
||||
const zone = await ZoneRepository.getById(zoneId)
|
||||
if (!zone) {
|
||||
return this.sendError(res, 'Zone not found', 404)
|
||||
}
|
||||
|
||||
const assets: AssetData[] = []
|
||||
const tiles = await TileRepository.getByZoneId(zoneId)
|
||||
|
||||
for (const tile of tiles) {
|
||||
assets.push({ key: tile.getId(), data: '/assets/tiles/' + tile.getId() + '.png', group: 'tiles', updatedAt: tile.getUpdatedAt() } as AssetData)
|
||||
}
|
||||
|
||||
return this.sendSuccess(res, assets)
|
||||
}
|
||||
|
||||
/**
|
||||
* List sprite actions
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async listSpriteActions(req: Request, res: Response) {
|
||||
const spriteId = req.params.spriteId as UUID
|
||||
|
||||
if (!spriteId) {
|
||||
return this.sendError(res, 'Invalid sprite ID', 400)
|
||||
}
|
||||
|
||||
const sprite = await SpriteRepository.getById(spriteId)
|
||||
if (!sprite) {
|
||||
return this.sendError(res, 'Sprite not found', 404)
|
||||
}
|
||||
|
||||
await Database.getEntityManager().populate(sprite, ['spriteActions'])
|
||||
|
||||
const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({
|
||||
key: sprite.id + '-' + spriteAction.action,
|
||||
data: '/assets/sprites/' + sprite.getId() + '/' + spriteAction.getAction() + '.png',
|
||||
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
|
||||
updatedAt: sprite.getUpdatedAt(),
|
||||
originX: Number(spriteAction.originX.toString()),
|
||||
originY: Number(spriteAction.originY.toString()),
|
||||
isAnimated: spriteAction.getIsAnimated(),
|
||||
frameRate: spriteAction.getFrameRate(),
|
||||
frameWidth: spriteAction.getFrameWidth(),
|
||||
frameHeight: spriteAction.getFrameHeight(),
|
||||
frameCount: JSON.parse(JSON.stringify(spriteAction.getSprites())).length
|
||||
}))
|
||||
|
||||
return this.sendSuccess(res, assets)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download asset
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async downloadAsset(req: Request, res: Response) {
|
||||
const { type, spriteId, file } = req.params
|
||||
|
||||
const assetPath = type === 'sprites' && spriteId ? Storage.getPublicPath(type, spriteId, file) : Storage.getPublicPath(type, file)
|
||||
|
||||
if (!fs.existsSync(assetPath)) {
|
||||
this.logger.error(`File not found: ${assetPath}`)
|
||||
return this.sendError(res, 'Asset not found', 404)
|
||||
}
|
||||
|
||||
res.sendFile(assetPath, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error sending file:' + err)
|
||||
this.sendError(res, 'Error downloading the asset', 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user