Compare commits

..

1 Commits

156 changed files with 4280 additions and 4202 deletions

View File

@ -3,7 +3,7 @@ ENV=development
HOST="0.0.0.0" HOST="0.0.0.0"
PORT=4000 PORT=4000
JWT_SECRET="secret" JWT_SECRET="secret"
CLIENT_URL="http://localhost:5173" CLIENT_URL="http://192.168.3.4:5173"
# Database configuration # Database configuration
REDIS_URL="redis://@127.0.0.1:6379/4" REDIS_URL="redis://@127.0.0.1:6379/4"

View File

@ -3,6 +3,6 @@
"semi": false, "semi": false,
"tabWidth": 2, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"printWidth": 200, "printWidth": 300,
"trailingComma": "none" "trailingComma": "none"
} }

View File

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

View File

@ -1,35 +1,19 @@
import { Migration } from '@mikro-orm/migrations'; import { Migration } from '@mikro-orm/migrations';
export class Migration20250123193515 extends Migration { export class Migration20250101224501 extends Migration {
override async up(): Promise<void> { override async up(): Promise<void> {
this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`map_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 \`map_effect\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_effect\` add index \`map_effect_map_id_index\`(\`map_id\`);`);
this.addSql(`create table \`map_event_tile\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_event_tile\` add index \`map_event_tile_map_id_index\`(\`map_id\`);`);
this.addSql(`create table \`map_event_tile_teleport\` (\`id\` varchar(255) not null, \`map_event_tile_id\` varchar(255) not null, \`to_map_id\` varchar(255) not null, \`to_rotation\` int not null, \`to_position_x\` int not null, \`to_position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`map_event_tile_teleport\` add unique \`map_event_tile_teleport_map_event_tile_id_unique\`(\`map_event_tile_id\`);`);
this.addSql(`alter table \`map_event_tile_teleport\` add index \`map_event_tile_teleport_to_map_id_index\`(\`to_map_id\`);`);
this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` numeric(10,2) not null default 0, \`origin_y\` numeric(10,2) not null default 0, \`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 \`placed_map_object\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_object_id_index\`(\`map_object_id\`);`);
this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) not null default '', \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`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(`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(`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(`alter table \`character_type\` add index \`character_type_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`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(`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(`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;`);
@ -45,16 +29,20 @@ export class Migration20250123193515 extends Migration {
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 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(`alter table \`password_reset_token\` add unique \`password_reset_token_token_unique\`(\`token\`);`);
this.addSql(`create table \`character\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`name\` varchar(255) not null, \`online\` tinyint(1) not null default false, \`role\` varchar(255) not null default 'player', \`map_id\` varchar(255) not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`rotation\` int not null default 0, \`character_type_id\` varchar(255) null, \`character_hair_id\` varchar(255) null, \`alignment\` int not null default 50, \`hitpoints\` int not null default 100, \`mana\` int not null default 100, \`level\` int not null default 1, \`experience\` int not null default 0, \`strength\` int not null default 10, \`dexterity\` int not null default 10, \`intelligence\` int not null default 10, \`wisdom\` int not null default 10, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`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 index \`character_user_id_index\`(\`user_id\`);`);
this.addSql(`alter table \`character\` add unique \`character_name_unique\`(\`name\`);`); this.addSql(`alter table \`character\` add unique \`character_name_unique\`(\`name\`);`);
this.addSql(`alter table \`character\` add index \`character_map_id_index\`(\`map_id\`);`); this.addSql(`alter table \`character\` add index \`character_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_type_id_index\`(\`character_type_id\`);`);
this.addSql(`alter table \`character\` add index \`character_character_hair_id_index\`(\`character_hair_id\`);`); this.addSql(`alter table \`character\` add index \`character_character_hair_id_index\`(\`character_hair_id\`);`);
this.addSql(`create table \`chat\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`message\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`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_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`chat\` add index \`chat_map_id_index\`(\`map_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(`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_character_id_index\`(\`character_id\`);`);
@ -64,17 +52,19 @@ export class Migration20250123193515 extends Migration {
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_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_item_id_index\`(\`character_item_id\`);`); this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_item_id_index\`(\`character_item_id\`);`);
this.addSql(`create table \`world\` (\`date\` datetime not null, \`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_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(`alter table \`map_effect\` add constraint \`map_effect_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`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(`alter table \`map_event_tile\` add constraint \`map_event_tile_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`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(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_map_event_tile_id_foreign\` foreign key (\`map_event_tile_id\`) references \`map_event_tile\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`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 \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_to_map_id_foreign\` foreign key (\`to_map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`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 \`placed_map_object\` add constraint \`placed_map_object_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`); this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
@ -87,18 +77,28 @@ export class Migration20250123193515 extends Migration {
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 \`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_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade;`); this.addSql(`alter table \`character\` add constraint \`character_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_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 \`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_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`chat\` add constraint \`chat_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`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_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_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_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 \`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;`);
} }
} }

View File

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

View File

@ -8,15 +8,15 @@ import serverConfig from './src/application/config'
export default defineConfig({ export default defineConfig({
extensions: [Migrator], extensions: [Migrator],
metadataProvider: TsMorphMetadataProvider, metadataProvider: TsMorphMetadataProvider,
entities: ['./src/entities/*.js'], entities: ['./src/entities/**/*.js'],
entitiesTs: ['./src/entities/*.ts'], entitiesTs: ['./src/entities/**/*.ts'],
driver: MySqlDriver, driver: MySqlDriver,
host: serverConfig.DB_HOST, host: serverConfig.DB_HOST,
port: serverConfig.DB_PORT, port: serverConfig.DB_PORT,
user: serverConfig.DB_USER, user: serverConfig.DB_USER,
password: serverConfig.DB_PASS, password: serverConfig.DB_PASS,
dbName: serverConfig.DB_NAME, dbName: serverConfig.DB_NAME,
debug: false, // debug: serverConfig.ENV !== 'production',
driverOptions: { driverOptions: {
allowPublicKeyRetrieval: true allowPublicKeyRetrieval: true
}, },

654
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
{ {
"useTsNode": true,
"alwaysAllowTs": true,
"scripts": { "scripts": {
"start": "node dist/server.js", "start": "node dist/server.js",
"dev": "nodemon --exec tsx src/server.ts", "dev": "nodemon --exec tsx src/server.ts",
@ -14,8 +16,10 @@
"@mikro-orm/mysql": "^6.4.2", "@mikro-orm/mysql": "^6.4.2",
"@mikro-orm/reflection": "^6.4.2", "@mikro-orm/reflection": "^6.4.2",
"@prisma/client": "^6.1.0", "@prisma/client": "^6.1.0",
"@types/blessed": "^0.1.25",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"blessed": "^0.1.81",
"bullmq": "^5.13.2", "bullmq": "^5.13.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",

Binary file not shown.

View File

@ -1,6 +1,4 @@
import fs from 'fs' import { Request, Response } from 'express'
import { Response } from 'express'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
@ -21,18 +19,4 @@ export abstract class BaseController {
message message
}) })
} }
protected sendFile(res: Response, filePath: string) {
if (!fs.existsSync(filePath)) {
this.logger.error(`File not found: ${filePath}`)
return this.sendError(res, 'Asset not found', 404)
}
res.sendFile(filePath, (error) => {
if (error) {
this.logger.error('Error sending file:' + error)
this.sendError(res, 'Error downloading the asset', 500)
}
})
}
} }

View File

@ -5,32 +5,23 @@ import Logger, { LoggerType } from '#application/logger'
export abstract class BaseEntity { export abstract class BaseEntity {
protected readonly logger = Logger.type(LoggerType.ENTITY) protected readonly logger = Logger.type(LoggerType.ENTITY)
protected entityManager?: EntityManager
private getEntityManager(): EntityManager { private getEntityManager(): EntityManager {
if (!this.entityManager) { return Database.getEntityManager()
this.entityManager = Database.getORM().em.fork()
}
return this.entityManager
}
public setEntityManager(entityManager: EntityManager) {
this.entityManager = entityManager
return this
} }
async save(): Promise<this> { async save(): Promise<this> {
return this.execute('persist', 'save entity') return this.execute('persist', 'save entity')
} }
async delete(): Promise<this> {
return this.execute('remove', 'remove entity')
}
async update(): Promise<this> { async update(): Promise<this> {
return this.execute('merge', 'update entity') 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> { private async execute(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise<this> {
try { try {
const em = this.getEntityManager() const em = this.getEntityManager()

View File

@ -2,8 +2,6 @@ import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
export abstract class BaseEvent { export abstract class BaseEvent {
protected readonly logger = Logger.type(LoggerType.GAME) protected readonly logger = Logger.type(LoggerType.GAME)
@ -13,16 +11,6 @@ export abstract class BaseEvent {
readonly socket: TSocket readonly socket: TSocket
) {} ) {}
protected async getCharacter(): Promise<Character | null> {
const characterRepository = new CharacterRepository()
return characterRepository.getById(this.socket.characterId!)
}
protected async isCharacterGM(): Promise<boolean> {
const character = await this.getCharacter()
return character?.getRole() === 'gm'
}
protected emitError(message: string): void { protected emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message }) this.socket.emit('notification', { title: 'Server message', message })
this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`) this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)

View File

@ -6,12 +6,8 @@ import Logger, { LoggerType } from '#application/logger'
export abstract class BaseRepository { export abstract class BaseRepository {
protected readonly logger = Logger.type(LoggerType.REPOSITORY) protected readonly logger = Logger.type(LoggerType.REPOSITORY)
private entityManager?: EntityManager
getEntityManager(): EntityManager { protected get em(): EntityManager {
if (!this.entityManager) { return Database.getEntityManager()
this.entityManager = Database.getORM().em.fork()
}
return this.entityManager
} }
} }

View File

@ -1,57 +0,0 @@
import * as fs from 'fs'
import * as path from 'path'
import { pathToFileURL } from 'url'
import Logger, { LoggerType } from '#application/logger'
import Storage from '#application/storage'
import { Command } from '#application/types'
export class CommandRegistry {
private readonly commands: Map<string, Command> = new Map()
private readonly logger = Logger.type(LoggerType.COMMAND)
public getCommand(name: string): Command | undefined {
return this.commands.get(name)
}
public async loadCommands(): Promise<void> {
const directory = Storage.getAppPath('commands')
this.logger.info(`Loading commands from: ${directory}`)
try {
const files = await fs.promises.readdir(directory, { withFileTypes: true })
await Promise.all(files.filter((file) => this.isValidCommandFile(file)).map((file) => this.loadCommandFile(file)))
} catch (error) {
this.logger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`)
}
}
private isValidCommandFile(file: fs.Dirent): boolean {
return file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.js'))
}
private async loadCommandFile(file: fs.Dirent): Promise<void> {
try {
const filePath = Storage.getAppPath('commands', file.name)
const commandName = path.basename(file.name, path.extname(file.name))
const module = await import(pathToFileURL(filePath).href)
if (typeof module.default !== 'function') {
this.logger.warn(`Unrecognized export in ${file.name}`)
return
}
this.registerCommand(commandName, module.default)
} catch (error) {
this.logger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
}
}
private registerCommand(name: string, CommandClass: Command): void {
if (this.commands.has(name)) {
this.logger.warn(`Command '${name}' is already registered. Overwriting...`)
}
this.commands.set(name, CommandClass)
this.logger.info(`Registered command: ${name}`)
}
}

View File

@ -1,33 +0,0 @@
import * as readline from 'readline'
export class ConsolePrompt {
private readonly rl: readline.Interface
private isClosed: boolean = false
constructor(private readonly commandHandler: (command: string) => void) {
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
this.rl.on('close', () => {
this.isClosed = true
})
}
public start(): void {
if (this.isClosed) return
this.promptCommand()
}
public close(): void {
this.rl.close()
}
private promptCommand(): void {
this.rl.question('> ', (command: string) => {
this.commandHandler(command)
this.promptCommand()
})
}
}

View File

@ -1,77 +0,0 @@
import * as fs from 'fs'
import * as path from 'path'
import Logger, { LoggerType } from '#application/logger'
export class LogReader {
private logger = Logger.type(LoggerType.CONSOLE)
private watchers: fs.FSWatcher[] = []
private readonly logsDirectory: string
constructor(rootPath: string) {
this.logsDirectory = path.join(rootPath, 'logs')
}
public start(): void {
this.logger.info('Starting log reader...')
this.watchLogs()
}
public stop(): void {
this.watchers.forEach((watcher) => watcher.close())
this.watchers = []
}
private watchLogs(): void {
// Watch directory for new files
const directoryWatcher = fs.watch(this.logsDirectory, (_, filename) => {
if (filename?.endsWith('.log')) {
this.watchLogFile(filename)
}
})
this.watchers.push(directoryWatcher)
// Watch existing files
try {
fs.readdirSync(this.logsDirectory)
.filter((file) => file.endsWith('.log'))
.forEach((file) => this.watchLogFile(file))
} catch (error) {
this.logger.error(`Error reading logs directory: ${error}`)
}
}
private watchLogFile(filename: string): void {
const filePath = path.join(this.logsDirectory, filename)
let currentPosition = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0
const watcher = fs.watch(filePath, () => {
try {
const stat = fs.statSync(filePath)
const newPosition = stat.size
if (newPosition < currentPosition) {
currentPosition = 0
}
if (newPosition > currentPosition) {
const stream = fs.createReadStream(filePath, {
start: currentPosition,
end: newPosition
})
stream.on('data', (data) => {
console.log(`[${filename}]`)
console.log(data.toString()) //
})
currentPosition = newPosition
}
} catch {
watcher.close()
}
})
this.watchers.push(watcher)
}
}

View File

@ -1,3 +1,4 @@
import { EntityManager } from '@mikro-orm/core'
import { MikroORM } from '@mikro-orm/mysql' import { MikroORM } from '@mikro-orm/mysql'
import Logger, { LoggerType } from './logger' import Logger, { LoggerType } from './logger'
@ -5,11 +6,13 @@ import config from '../../mikro-orm.config'
class Database { class Database {
private static orm: MikroORM private static orm: MikroORM
private static em: EntityManager
private static logger = Logger.type(LoggerType.APP) private static logger = Logger.type(LoggerType.APP)
public static async initialize(): Promise<void> { public static async initialize(): Promise<void> {
try { try {
this.orm = await MikroORM.init(config) Database.orm = await MikroORM.init(config)
Database.em = Database.orm.em.fork()
this.logger.info('Database connection initialized') this.logger.info('Database connection initialized')
} catch (error) { } catch (error) {
this.logger.error(`MikroORM connection failed: ${error}`) this.logger.error(`MikroORM connection failed: ${error}`)
@ -18,7 +21,17 @@ class Database {
} }
public static getORM(): MikroORM { public static getORM(): MikroORM {
return this.orm 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
} }
} }

View File

@ -55,7 +55,7 @@ export enum CharacterEquipmentSlotType {
RING = 'RING' RING = 'RING'
} }
export enum MapEventTileType { export enum ZoneEventTileType {
BLOCK = 'BLOCK', BLOCK = 'BLOCK',
TELEPORT = 'TELEPORT', TELEPORT = 'TELEPORT',
NPC = 'NPC', NPC = 'NPC',

View File

@ -1,8 +1,8 @@
import { Server, Socket } from 'socket.io' import { Server, Socket } from 'socket.io'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { MapEventTile } from '#entities/mapEventTile' import { ZoneEventTile } from '#entities/zoneEventTile'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport' import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
export type UUID = `${string}-${string}-${string}-${string}-${string}` export type UUID = `${string}-${string}-${string}-${string}-${string}`
@ -26,8 +26,8 @@ export type ExtendedCharacter = Character & {
resetMovement?: boolean resetMovement?: boolean
} }
export type MapEventTileWithTeleport = MapEventTile & { export type ZoneEventTileWithTeleport = ZoneEventTile & {
teleport: MapEventTileTeleport teleport: ZoneEventTileTeleport
} }
export type AssetData = { export type AssetData = {
@ -37,6 +37,7 @@ export type AssetData = {
updatedAt: Date updatedAt: Date
originX?: number originX?: number
originY?: number originY?: number
isAnimated?: boolean
frameRate?: number frameRate?: number
frameWidth?: number frameWidth?: number
frameHeight?: number frameHeight?: number
@ -45,11 +46,8 @@ export type AssetData = {
export type WorldSettings = { export type WorldSettings = {
date: Date date: Date
weatherState: WeatherState isRainEnabled: boolean
} isFogEnabled: boolean
export type WeatherState = {
rainPercentage: number
fogDensity: number fogDensity: number
} }

View File

@ -9,35 +9,31 @@ import { UUID } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { CharacterHair } from '#entities/characterHair' import { CharacterHair } from '#entities/characterHair'
import { CharacterType } from '#entities/characterType' import { CharacterType } from '#entities/characterType'
import { Map } from '#entities/map'
import { MapEffect } from '#entities/mapEffect'
import { MapObject } from '#entities/mapObject' import { MapObject } from '#entities/mapObject'
import { Sprite } from '#entities/sprite' import { Sprite } from '#entities/sprite'
import { SpriteAction } from '#entities/spriteAction' import { SpriteAction } from '#entities/spriteAction'
import { Tile } from '#entities/tile' import { Tile } from '#entities/tile'
import { User } from '#entities/user' import { User } from '#entities/user'
import { Zone } from '#entities/zone'
import { ZoneEffect } from '#entities/zoneEffect'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository'
import MapRepository from '#repositories/mapRepository' import ZoneRepository from '#repositories/zoneRepository'
// @TODO : Replace this with seeding // @TODO : Replace this with seeding
// https://mikro-orm.io/docs/seeding // https://mikro-orm.io/docs/seeding
export default class InitCommand extends BaseCommand { export default class InitCommand extends BaseCommand {
private readonly mapRepository = new MapRepository()
private readonly characterTypeRepository = new CharacterTypeRepository()
private readonly characterHairRepository = new CharacterHairRepository()
public async execute(): Promise<void> { public async execute(): Promise<void> {
// Assets // Assets
await this.importTiles() await this.importTiles()
await this.importMapObjects() await this.importObjects()
await this.createCharacterType() await this.createCharacterType()
await this.createCharacterHair() await this.createCharacterHair()
// await this.createCharacterEquipment() // await this.createCharacterEquipment()
// Map // Zone
await this.createMap() await this.createZone()
// User // User
await this.createUser() await this.createUser()
@ -55,19 +51,19 @@ export default class InitCommand extends BaseCommand {
} }
} }
private async importMapObjects(): Promise<void> { private async importObjects(): Promise<void> {
for (const mapObject of fs.readdirSync(Storage.getPublicPath('map_objects'))) { for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) {
const newMapObject = new MapObject() const newMapObject = new MapObject()
newMapObject newMapObject
.setId(mapObject.split('.')[0] as UUID) .setId(object.split('.')[0] as UUID)
.setName('New map object') .setName('New object')
.setFrameWidth( .setFrameWidth(
(await sharp(Storage.getPublicPath('map_objects', mapObject)) (await sharp(Storage.getPublicPath('objects', object))
.metadata() .metadata()
.then((metadata) => metadata.height)) ?? 0 .then((metadata) => metadata.height)) ?? 0
) )
.setFrameHeight( .setFrameHeight(
(await sharp(Storage.getPublicPath('map_objects', mapObject)) (await sharp(Storage.getPublicPath('objects', object))
.metadata() .metadata()
.then((metadata) => metadata.width)) ?? 0 .then((metadata) => metadata.width)) ?? 0
) )
@ -89,6 +85,8 @@ export default class InitCommand extends BaseCommand {
]) ])
.setOriginX(0) .setOriginX(0)
.setOriginY(0) .setOriginY(0)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64) .setFrameWidth(64)
.setFrameHeight(94) .setFrameHeight(94)
.setFrameRate(0) .setFrameRate(0)
@ -103,6 +101,8 @@ export default class InitCommand extends BaseCommand {
]) ])
.setOriginX(0) .setOriginX(0)
.setOriginY(0) .setOriginY(0)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64) .setFrameWidth(64)
.setFrameHeight(94) .setFrameHeight(94)
.setFrameRate(0) .setFrameRate(0)
@ -120,6 +120,8 @@ export default class InitCommand extends BaseCommand {
]) ])
.setOriginX(0) .setOriginX(0)
.setOriginY(0) .setOriginY(0)
.setIsAnimated(true)
.setIsLooping(false)
.setFrameWidth(64) .setFrameWidth(64)
.setFrameHeight(94) .setFrameHeight(94)
.setFrameRate(7) .setFrameRate(7)
@ -137,6 +139,8 @@ export default class InitCommand extends BaseCommand {
]) ])
.setOriginX(0) .setOriginX(0)
.setOriginY(0) .setOriginY(0)
.setIsAnimated(true)
.setIsLooping(false)
.setFrameWidth(64) .setFrameWidth(64)
.setFrameHeight(94) .setFrameHeight(94)
.setFrameRate(7) .setFrameRate(7)
@ -144,14 +148,7 @@ export default class InitCommand extends BaseCommand {
.save() .save()
const characterType = new CharacterType() const characterType = new CharacterType()
await characterType await characterType.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0').setName('New character type').setGender(CharacterGender.MALE).setRace(CharacterRace.HUMAN).setIsSelectable(true).setSprite(characterSprite).save()
.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> { private async createCharacterHair(): Promise<void> {
@ -167,6 +164,8 @@ export default class InitCommand extends BaseCommand {
]) ])
.setOriginX(0.5) .setOriginX(0.5)
.setOriginY(5.34) .setOriginY(5.34)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64) .setFrameWidth(64)
.setFrameHeight(18) .setFrameHeight(18)
.setFrameRate(0) .setFrameRate(0)
@ -181,6 +180,8 @@ export default class InitCommand extends BaseCommand {
]) ])
.setOriginX(0.5) .setOriginX(0.5)
.setOriginY(4.34) .setOriginY(4.34)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64) .setFrameWidth(64)
.setFrameHeight(22) .setFrameHeight(22)
.setFrameRate(0) .setFrameRate(0)
@ -204,6 +205,8 @@ export default class InitCommand extends BaseCommand {
sprites: ['data:image/png;base64,...'], sprites: ['data:image/png;base64,...'],
originX: 0, originX: 0,
originY: 0, originY: 0,
isAnimated: false,
isLooping: false,
frameWidth: 64, frameWidth: 64,
frameHeight: 94, frameHeight: 94,
frameRate: 0 frameRate: 0
@ -221,17 +224,17 @@ export default class InitCommand extends BaseCommand {
await equipmentSprite.save() await equipmentSprite.save()
} }
private async createMap(): Promise<void> { private async createZone(): Promise<void> {
const map = new Map() const zone = new Zone()
await map await zone
.setName('New map') .setName('New zone')
.setWidth(100) .setWidth(100)
.setHeight(100) .setHeight(100)
.setTiles(Array.from({ length: 100 }, () => Array.from({ length: 100 }, () => 'a2fd8d6f-5042-437a-9c1e-c66b91ecc35b'))) .setTiles(Array.from({ length: 100 }, () => Array.from({ length: 100 }, () => 'a2fd8d6f-5042-437a-9c1e-c66b91ecc35b')))
.save() .save()
const effect = new MapEffect() const effect = new ZoneEffect()
await effect.setEffect('light').setStrength(100).setMap(map).save() await effect.setEffect('light').setStrength(100).setZone(zone).save()
} }
private async createUser(): Promise<void> { private async createUser(): Promise<void> {
@ -244,9 +247,9 @@ export default class InitCommand extends BaseCommand {
.setUser(user) .setUser(user)
.setName('root') .setName('root')
.setRole('gm') .setRole('gm')
.setMap((await this.mapRepository.getFirst())!) .setZone((await ZoneRepository.getFirst())!)
.setCharacterType(await this.characterTypeRepository.getFirst()) .setCharacterType((await CharacterTypeRepository.getFirst()) ?? undefined)
.setCharacterHair(await this.characterHairRepository.getFirst()) .setCharacterHair((await CharacterHairRepository.getFirst()) ?? undefined)
.save() .save()
} }
} }

View File

@ -1,10 +0,0 @@
import { BaseCommand } from '#application/base/baseCommand'
import MapManager from '#managers/mapManager'
type CommandInput = string[]
export default class ListMapsCommand extends BaseCommand {
public execute(input: CommandInput): void {
console.log(MapManager.getLoadedMaps())
}
}

12
src/commands/listZones.ts Normal file
View File

@ -0,0 +1,12 @@
import { Server } from 'socket.io'
import { BaseCommand } from '#application/base/baseCommand'
import ZoneManager from '#managers/zoneManager'
type CommandInput = string[]
export default class ListZonesCommand extends BaseCommand {
public execute(input: CommandInput): void {
console.log(ZoneManager.getLoadedZones())
}
}

View File

@ -1,119 +0,0 @@
import { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import MapObjectRepository from '#repositories/mapObjectRepository'
import MapRepository from '#repositories/mapRepository'
import SpriteRepository from '#repositories/spriteRepository'
import TileRepository from '#repositories/tileRepository'
export class CacheController extends BaseController {
/**
* Serve a list of tiles and send as JSON
* @param req
* @param res
*/
public async tiles(req: Request, res: Response) {
const items: any[] = []
const tileRepository = new TileRepository()
const tiles = await tileRepository.getAll()
for (const tile of tiles) {
items.push(await tile.cache())
}
return this.sendSuccess(res, items)
}
/**
* Serve a list of maps and send as JSON
* @param req
* @param res
*/
public async maps(req: Request, res: Response) {
const items: any[] = []
const mapRepository = new MapRepository()
const maps = await mapRepository.getAll()
for (const map of maps) {
items.push(await map.cache())
}
return this.sendSuccess(res, items)
}
/**
* Serve a list of map objects and send as JSON
* @param req
* @param res
*/
public async mapObjects(req: Request, res: Response) {
const items: any[] = []
const mapObjectRepository = new MapObjectRepository()
const mapObjects = await mapObjectRepository.getAll()
for (const mapObject of mapObjects) {
items.push(await mapObject.cache())
}
return this.sendSuccess(res, items)
}
/**
* Serve a list of character hairs and send as JSON
* @param req
* @param res
*/
public async characterHair(req: Request, res: Response) {
const items: any[] = []
const characterHairRepository = new CharacterHairRepository()
const characterHairs = await characterHairRepository.getAll()
for (const characterHair of characterHairs) {
items.push(await characterHair.cache())
}
return this.sendSuccess(res, items)
}
/**
* Serve a list of character types and send as JSON
* @param req
* @param res
*/
public async characterTypes(req: Request, res: Response) {
const items: any[] = []
const characterTypeRepository = new CharacterTypeRepository()
const characterTypes = await characterTypeRepository.getAll()
for (const characterType of characterTypes) {
items.push(await characterType.cache())
}
return this.sendSuccess(res, items)
}
/**
* Serve a list of sprites and send as JSON
* @param req
* @param res
*/
public async sprites(req: Request, res: Response) {
const items: any[] = []
const spriteRepository = new SpriteRepository()
const sprites = await spriteRepository.getAll()
for (const sprite of sprites) {
items.push(await sprite.cache())
}
return this.sendSuccess(res, items)
}
}

View File

@ -1,19 +0,0 @@
import { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import Storage from '#application/storage'
export class TexturesController extends BaseController {
/**
* Download texture
* @param req
* @param res
*/
public async download(req: Request, res: Response) {
const { type, spriteId, file } = req.params
const texture = type === 'sprites' && spriteId ? Storage.getPublicPath(type, spriteId, file) : Storage.getPublicPath(type, file)
this.sendFile(res, texture)
}
}

View File

@ -1,295 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { CharacterEquipment } from '#entities/characterEquipment'
import { CharacterHair } from '#entities/characterHair'
import { CharacterItem } from '#entities/characterItem'
import { CharacterType } from '#entities/characterType'
import { Chat } from '#entities/chat'
import { Map } from '#entities/map'
import { User } from '#entities/user'
export class BaseCharacter extends BaseEntity {
@PrimaryKey()
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 - @TODO: Update to spawn point when current map is not found
@ManyToOne()
map!: Map
@Property()
positionX = 0
@Property()
positionY = 0
@Property()
rotation = 0
// Customization
@ManyToOne({ deleteRule: 'set null' })
characterType: CharacterType | null = null
@ManyToOne({ deleteRule: 'set null' })
characterHair: CharacterHair | null = null
// 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
}
setMap(map: Map) {
this.map = map
return this
}
getMap() {
return this.map
}
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) {
this.characterType = characterType
return this
}
getCharacterType() {
return this.characterType
}
setCharacterHair(characterHair: CharacterHair | null) {
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
}
}

View File

@ -1,59 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Enum, ManyToOne, PrimaryKey } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterEquipmentSlotType } from '#application/enums'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { CharacterItem } from '#entities/characterItem'
export class BaseCharacterEquipment extends BaseEntity {
@PrimaryKey()
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
}
}

View File

@ -1,95 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterGender } from '#application/enums'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Sprite } from '#entities/sprite'
export class BaseCharacterHair extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property()
gender: CharacterGender = CharacterGender.MALE
@Property()
isSelectable = false
@ManyToOne()
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
}
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
}
}

View File

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

View File

@ -1,107 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterGender, CharacterRace } from '#application/enums'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Sprite } from '#entities/sprite'
export class BaseCharacterType extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Enum(() => CharacterGender)
gender!: CharacterGender
@Enum(() => CharacterRace)
race!: CharacterRace
@Property()
isSelectable = false
@ManyToOne()
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
}
}

View File

@ -1,70 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Map } from '#entities/map'
export class BaseChat extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
character!: Character
@ManyToOne({ deleteRule: 'cascade' })
map!: Map
@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
}
setMap(map: Map) {
this.map = map
return this
}
getMap() {
return this.map
}
setMessage(message: string) {
this.message = message
return this
}
getMessage() {
return this.message
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
}

View File

@ -1,119 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { ItemType, ItemRarity } from '#application/enums'
import { UUID } from '#application/types'
import { CharacterItem } from '#entities/characterItem'
import { Sprite } from '#entities/sprite'
export class BaseItem extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property()
description: string = ''
@Enum(() => ItemType)
itemType!: ItemType
@Property()
stackable = false
@Enum(() => ItemRarity)
rarity: ItemRarity = ItemRarity.COMMON
@ManyToOne(() => Sprite)
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
}
}

View File

@ -1,143 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { MapEffect } from '#entities/mapEffect'
import { MapEventTile } from '#entities/mapEventTile'
import { PlacedMapObject } from '#entities/placedMapObject'
export class BaseMap extends BaseEntity {
@PrimaryKey()
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(() => MapEffect, (effect) => effect.map, { orphanRemoval: true })
mapEffects = new Collection<MapEffect>(this)
@OneToMany(() => MapEventTile, (tile) => tile.map, { orphanRemoval: true })
mapEventTiles = new Collection<MapEventTile>(this)
@OneToMany(() => PlacedMapObject, (placedMapObject) => placedMapObject.map, { orphanRemoval: true })
placedMapObjects = new Collection<PlacedMapObject>(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
}
setMapEffects(mapEffects: Collection<MapEffect>) {
this.mapEffects = mapEffects
return this
}
getMapEffects() {
return this.mapEffects
}
setMapEventTiles(mapEventTiles: Collection<MapEventTile>) {
this.mapEventTiles = mapEventTiles
return this
}
getMapEventTiles() {
return this.mapEventTiles
}
setPlacedMapObjects(placedMapObjects: Collection<PlacedMapObject>) {
this.placedMapObjects = placedMapObjects
return this
}
getPlacedMapObjects() {
return this.placedMapObjects
}
}

View File

@ -1,128 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
export class BaseMapObject extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property({ type: 'json', nullable: true })
tags?: any
@Property({ type: 'decimal', precision: 10, scale: 2 })
originX = 0
@Property({ type: 'decimal', precision: 10, scale: 2 })
originY = 0
@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
}
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
}
}

View File

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

View File

@ -1,69 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { SpriteAction } from '#entities/spriteAction'
export class BaseSprite 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
}
}

View File

@ -1,117 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { Sprite } from '#entities/sprite'
export class BaseSpriteAction 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()
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
}
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
}
}

View File

@ -1,68 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
export class BaseTile 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
}
}

View File

@ -1,96 +0,0 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import bcrypt from 'bcryptjs'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { PasswordResetToken } from '#entities/passwordResetToken'
export class BaseUser 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
}
}

View File

@ -1,41 +0,0 @@
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
export class BaseWorld extends BaseEntity {
@PrimaryKey()
date = new Date()
@Property()
rainPercentage = 0
@Property()
fogDensity = 0
setDate(date: Date) {
this.date = date
return this
}
getDate() {
return this.date
}
setRainPercentage(rainPercentage: number) {
this.rainPercentage = rainPercentage
return this
}
getRainPercentage() {
return this.rainPercentage
}
setFogDensity(fogDensity: number) {
this.fogDensity = fogDensity
return this
}
getFogDensity() {
return this.fogDensity
}
}

View File

@ -1,6 +1,297 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseCharacter } from '#entities/base/character' 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() @Entity()
export class Character extends BaseCharacter {} 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
}
}

View File

@ -1,6 +1,61 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseCharacterEquipment } from '#entities/base/characterEquipment' 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() @Entity()
export class CharacterEquipment extends BaseCharacterEquipment {} 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
}
}

View File

@ -1,15 +1,73 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseCharacterHair } from '#entities/base/characterHair' 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() @Entity()
export class CharacterHair extends BaseCharacterHair { export class CharacterHair extends BaseEntity {
public async cache() { @PrimaryKey()
try { id = randomUUID()
return this
} catch (error) { @Property()
console.error(error) name!: string
return {}
} @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
} }
} }

View File

@ -1,6 +1,61 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseCharacterItem } from '#entities/base/characterItem' 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() @Entity()
export class CharacterItem extends BaseCharacterItem {} 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
}
}

View File

@ -1,15 +1,121 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseCharacterType } from '#entities/base/characterType' 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() @Entity()
export class CharacterType extends BaseCharacterType { export class CharacterType extends BaseEntity {
public async cache() { @PrimaryKey()
try { id = randomUUID()
return this
} catch (error) { @Property()
console.error(error) name!: string
return {}
} @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
} }
} }

View File

@ -1,6 +1,72 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseChat } from '#entities/base/chat' 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() @Entity()
export class Chat extends BaseChat {} 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
}
}

View File

@ -1,6 +1,121 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseItem } from '#entities/base/item' 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() @Entity()
export class Item extends BaseItem {} 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
}
}

View File

@ -1,39 +0,0 @@
import { Entity } from '@mikro-orm/core'
import { BaseMap } from '#entities/base/map'
export type MapCacheT = ReturnType<Map['cache']> | {}
@Entity()
export class Map extends BaseMap {
public async cache() {
try {
await this.getPlacedMapObjects().load()
await this.getMapEffects().load()
return {
id: this.getId(),
name: this.getName(),
width: this.getWidth(),
height: this.getHeight(),
tiles: this.getTiles(),
pvp: this.getPvp(),
updatedAt: this.getUpdatedAt(),
placedMapObjects: this.getPlacedMapObjects().map((placedMapObject) => ({
id: placedMapObject.getId(),
mapObject: placedMapObject.getMapObject().getId(),
isRotated: placedMapObject.getIsRotated(),
positionX: placedMapObject.getPositionX(),
positionY: placedMapObject.getPositionY()
})),
mapEffects: this.getMapEffects().map((mapEffect) => ({
effect: mapEffect.getEffect(),
strength: mapEffect.getStrength()
}))
}
} catch (error) {
console.error(error)
return {}
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,15 +1,143 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseMapObject } from '#entities/base/mapObject' 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() @Entity()
export class MapObject extends BaseMapObject { export class MapObject extends BaseEntity {
public async cache() { @PrimaryKey()
try { id = randomUUID()
return this
} catch (error) { @Property()
console.error(error) name!: string
return {}
} @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
} }
} }

View File

@ -1,6 +1,59 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BasePasswordResetToken } from '#entities/base/passwordResetToken' 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() @Entity()
export class PasswordResetToken extends BasePasswordResetToken {} 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
}
}

View File

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

View File

@ -1,32 +1,71 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseSprite } from '#entities/base/sprite' 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() @Entity()
export class Sprite extends BaseSprite { export class Sprite extends BaseEntity {
public async cache() { @PrimaryKey()
await this.getSpriteActions().load() id = randomUUID()
try { @Property()
return { name!: string
id: this.getId(),
name: this.getName(), @OneToMany(() => SpriteAction, (action) => action.sprite)
createdAt: this.getCreatedAt(), spriteActions = new Collection<SpriteAction>(this)
updatedAt: this.getUpdatedAt(),
spriteActions: this.getSpriteActions().map((spriteAction) => ({ @Property()
id: spriteAction.getId(), createdAt = new Date()
action: spriteAction.getAction(),
originX: spriteAction.getOriginX(), @Property()
originY: spriteAction.getOriginY(), updatedAt = new Date()
frameWidth: spriteAction.getFrameWidth(),
frameHeight: spriteAction.getFrameHeight(), setId(id: UUID) {
frameRate: spriteAction.getFrameRate(), this.id = id
frameCount: spriteAction.getSprites()?.length return this
})) }
}
} catch (error) { getId() {
console.error(error) return this.id
return {} }
}
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
} }
} }

View File

@ -1,6 +1,143 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseSpriteAction } from '#entities/base/spriteAction' 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() @Entity()
export class SpriteAction extends BaseSpriteAction {} 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
}
}

View File

@ -1,15 +1,69 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseTile } from '#entities/base/tile' import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity() @Entity()
export class Tile extends BaseTile { export class Tile extends BaseEntity {
public async cache() { @PrimaryKey()
try { id = randomUUID()
return this
} catch (error) { @Property()
console.error(error) name!: string
return {}
} @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
} }
} }

View File

@ -1,6 +1,98 @@
import { Entity } from '@mikro-orm/core' import { randomUUID } from 'node:crypto'
import { BaseUser } from '#entities/base/user' 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() @Entity()
export class User extends BaseUser {} 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
}
}

View File

@ -1,6 +1,66 @@
import { Entity } from '@mikro-orm/core' import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseWorld } from '#entities/base/world' import { BaseEntity } from '#application/base/baseEntity'
@Entity() @Entity()
export class World extends BaseWorld {} 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
View 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
}
}

View File

@ -2,16 +2,18 @@ import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Zone } from './zone'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Map } from '#entities/map'
export class BaseMapEffect extends BaseEntity { @Entity()
export class ZoneEffect extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
map!: Map zone!: Zone
@Property() @Property()
effect!: string effect!: string
@ -28,13 +30,13 @@ export class BaseMapEffect extends BaseEntity {
return this.id return this.id
} }
setMap(map: Map) { setZone(zone: Zone) {
this.map = map this.zone = zone
return this return this
} }
getMap() { getZone() {
return this.map return this.zone
} }
setEffect(effect: string) { setEffect(effect: string) {

View File

@ -2,21 +2,23 @@ import { randomUUID } from 'node:crypto'
import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity' import { Zone } from './zone'
import { MapEventTileType } from '#application/enums' import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
export class BaseMapEventTile extends BaseEntity { import { BaseEntity } from '#application/base/baseEntity'
import { ZoneEventTileType } from '#application/enums'
import { UUID } from '#application/types'
@Entity()
export class ZoneEventTile extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
map!: Map zone!: Zone
@Enum(() => MapEventTileType) @Enum(() => ZoneEventTileType)
type!: MapEventTileType type!: ZoneEventTileType
@Property() @Property()
positionX!: number positionX!: number
@ -24,8 +26,8 @@ export class BaseMapEventTile extends BaseEntity {
@Property() @Property()
positionY!: number positionY!: number
@OneToOne(() => MapEventTileTeleport, (teleport) => teleport.mapEventTile, { eager: true }) @OneToOne(() => ZoneEventTileTeleport, (teleport) => teleport.zoneEventTile)
teleport?: MapEventTileTeleport teleport?: ZoneEventTileTeleport
setId(id: UUID) { setId(id: UUID) {
this.id = id this.id = id
@ -36,16 +38,16 @@ export class BaseMapEventTile extends BaseEntity {
return this.id return this.id
} }
setMap(map: Map) { setZone(zone: Zone) {
this.map = map this.zone = zone
return this return this
} }
getMap() { getZone() {
return this.map return this.zone
} }
setType(type: MapEventTileType) { setType(type: ZoneEventTileType) {
this.type = type this.type = type
return this return this
} }
@ -72,7 +74,7 @@ export class BaseMapEventTile extends BaseEntity {
return this.positionY return this.positionY
} }
setTeleport(teleport: MapEventTileTeleport) { setTeleport(teleport: ZoneEventTileTeleport) {
this.teleport = teleport this.teleport = teleport
return this return this
} }

View File

@ -2,20 +2,22 @@ import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' 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 { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEventTile } from '#entities/mapEventTile'
export class BaseMapEventTileTeleport extends BaseEntity { @Entity()
export class ZoneEventTileTeleport extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@OneToOne({ deleteRule: 'cascade' }) @OneToOne({ deleteRule: 'cascade' })
mapEventTile!: MapEventTile zoneEventTile!: ZoneEventTile
@ManyToOne({ deleteRule: 'cascade', eager: true }) @ManyToOne({ deleteRule: 'cascade' })
toMap!: Map toZone!: Zone
@Property() @Property()
toRotation!: number toRotation!: number
@ -35,22 +37,22 @@ export class BaseMapEventTileTeleport extends BaseEntity {
return this.id return this.id
} }
setMapEventTile(mapEventTile: MapEventTile) { setZoneEventTile(zoneEventTile: ZoneEventTile) {
this.mapEventTile = mapEventTile this.zoneEventTile = zoneEventTile
return this return this
} }
getMapEventTile() { getZoneEventTile() {
return this.mapEventTile return this.zoneEventTile
} }
setToMap(toMap: Map) { setToZone(toZone: Zone) {
this.toMap = toMap this.toZone = toZone
return this return this
} }
getToMap() { getToZone() {
return this.toMap return this.toZone
} }
setToRotation(toRotation: number) { setToRotation(toRotation: number) {

View File

@ -2,23 +2,27 @@ import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Zone } from './zone'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapObject } from '#entities/mapObject' import { MapObject } from '#entities/mapObject'
//@TODO : Rename mapObject //@TODO : Rename mapObject
@Entity()
export class BasePlacedMapObject extends BaseEntity { export class ZoneObject extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
map!: Map zone!: Zone
@ManyToOne({ deleteRule: 'cascade', eager: true }) @ManyToOne({ deleteRule: 'cascade' })
mapObject!: MapObject mapObject!: MapObject
@Property()
depth = 0
@Property() @Property()
isRotated = false isRotated = false
@ -37,13 +41,13 @@ export class BasePlacedMapObject extends BaseEntity {
return this.id return this.id
} }
setMap(map: Map) { setZone(zone: Zone) {
this.map = map this.zone = zone
return this return this
} }
getMap() { getZone() {
return this.map return this.zone
} }
setMapObject(mapObject: MapObject) { setMapObject(mapObject: MapObject) {
@ -55,6 +59,15 @@ export class BasePlacedMapObject extends BaseEntity {
return this.mapObject return this.mapObject
} }
setDepth(depth: number) {
this.depth = depth
return this
}
getDepth() {
return this.depth
}
setIsRotated(isRotated: boolean) { setIsRotated(isRotated: boolean) {
this.isRotated = isRotated this.isRotated = isRotated
return this return this

View 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)
}
}

View File

@ -1,6 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import MapManager from '#managers/mapManager' import ZoneManager from '#managers/zoneManager'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
import TeleportService from '#services/teleportService' import TeleportService from '#services/teleportService'
@ -11,21 +11,34 @@ interface CharacterConnectPayload {
} }
export default class CharacterConnectEvent extends BaseEvent { export default class CharacterConnectEvent extends BaseEvent {
private readonly characterHairRepository = new CharacterHairRepository()
private readonly characterRepository = new CharacterRepository()
public listen(): void { public listen(): void {
this.socket.on('character:connect', this.handleEvent.bind(this)) 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> { private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {
if (!this.socket.userId) {
this.emitError('User not authenticated')
return
}
try { try {
if (await this.checkForActiveCharacters()) { if (await this.checkForActiveCharacters()) {
this.emitError('You are already connected to another character') this.emitError('You are already connected to another character')
return return
} }
const character = await this.characterRepository.getByUserAndId(this.socket.userId!, data.characterId) const character = await CharacterRepository.getByUserAndId(this.socket.userId, data.characterId)
if (!character) { if (!character) {
this.emitError('Character not found or does not belong to this user') this.emitError('Character not found or does not belong to this user')
@ -37,18 +50,18 @@ export default class CharacterConnectEvent extends BaseEvent {
// Set character hair // Set character hair
if (data.characterHairId !== undefined && data.characterHairId !== null) { if (data.characterHairId !== undefined && data.characterHairId !== null) {
const characterHair = await this.characterHairRepository.getById(data.characterHairId) const characterHair = await CharacterHairRepository.getById(data.characterHairId)
await character.setCharacterHair(characterHair).save() await character.setCharacterHair(characterHair).update()
} }
// Emit character connect event // Emit character connect event
callback({ character }) callback({ character })
// wait 300 ms, @TODO: Find a better way to do this, race condition // wait 300 ms, @TODO: Find a better way to do this
await new Promise((resolve) => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 100))
await TeleportService.teleportCharacter(character.id, { await TeleportService.teleportCharacter(character.id, {
targetMapId: character.map.id, targetZoneId: character.zone.id,
targetX: character.positionX, targetX: character.positionX,
targetY: character.positionY, targetY: character.positionY,
rotation: character.rotation, rotation: character.rotation,
@ -61,7 +74,7 @@ export default class CharacterConnectEvent extends BaseEvent {
} }
private async checkForActiveCharacters(): Promise<boolean> { private async checkForActiveCharacters(): Promise<boolean> {
const characters = await this.characterRepository.getByUserId(this.socket.userId!) const characters = await CharacterRepository.getByUserId(this.socket.userId!)
return characters?.some((char) => MapManager.getCharacterById(char.id)) ?? false return characters?.some((char) => ZoneManager.getCharacterById(char.id)) ?? false
} }
} }

View File

@ -4,8 +4,8 @@ import { BaseEvent } from '#application/base/baseEvent'
import { ZCharacterCreate } from '#application/zodTypes' import { ZCharacterCreate } from '#application/zodTypes'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
import MapRepository from '#repositories/mapRepository'
import UserRepository from '#repositories/userRepository' import UserRepository from '#repositories/userRepository'
import ZoneRepository from '#repositories/zoneRepository'
export default class CharacterCreateEvent extends BaseEvent { export default class CharacterCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
@ -17,34 +17,30 @@ export default class CharacterCreateEvent extends BaseEvent {
try { try {
data = ZCharacterCreate.parse(data) data = ZCharacterCreate.parse(data)
const userRepository = new UserRepository() const user = await UserRepository.getById(this.socket.userId!)
const characterRepository = new CharacterRepository()
const mapRepository = new MapRepository()
const user = await userRepository.getById(this.socket.userId!)
if (!user) { if (!user) {
return this.socket.emit('notification', { message: 'User not found' }) return this.socket.emit('notification', { message: 'User not found' })
} }
// Check if character name already exists // Check if character name already exists
const characterExists = await characterRepository.getByName(data.name) const characterExists = await CharacterRepository.getByName(data.name)
if (characterExists) { if (characterExists) {
return this.socket.emit('notification', { message: 'Character name already exists' }) return this.socket.emit('notification', { message: 'Character name already exists' })
} }
let characters: Character[] = await characterRepository.getByUserId(user.getId()) let characters: Character[] = await CharacterRepository.getByUserId(user.getId())
if (characters.length >= 4) { if (characters.length >= 4) {
return this.socket.emit('notification', { message: 'You can only have 4 characters' }) return this.socket.emit('notification', { message: 'You can only have 4 characters' })
} }
// @TODO: Change to default location // @TODO: Change to default location
const map = await mapRepository.getFirst() const zone = await ZoneRepository.getFirst()
const newCharacter = new Character() const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setMap(map!).save() await newCharacter.setName(data.name).setUser(user).setZone(zone!).save()
if (!newCharacter) { if (!newCharacter) {
return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' }) return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' })

View File

@ -1,6 +1,7 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { Zone } from '#entities/zone'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
type TypePayload = { type TypePayload = {
@ -8,6 +9,7 @@ type TypePayload = {
} }
type TypeResponse = { type TypeResponse = {
zone: Zone
characters: Character[] characters: Character[]
} }
@ -18,9 +20,12 @@ export default class CharacterDeleteEvent extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> { private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
try { try {
const characterRepository = new CharacterRepository() const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId)
await (await characterRepository.getByUserAndId(this.socket.userId!, data.characterId))?.delete() if (character) {
const characters: Character[] = await characterRepository.getByUserId(this.socket.userId!) await character.delete()
}
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
this.socket.emit('character:list', characters) this.socket.emit('character:list', characters)
} catch (error: any) { } catch (error: any) {

View File

@ -1,4 +1,5 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import Database from '#application/database'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
@ -9,8 +10,8 @@ export default class CharacterListEvent extends BaseEvent {
private async handleEvent(data: any): Promise<void> { private async handleEvent(data: any): Promise<void> {
try { try {
const characterRepository = new CharacterRepository() const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
let characters: Character[] = await characterRepository.getByUserId(this.socket.userId!) await Database.getEntityManager().populate(characters, ['characterType', 'characterHair'])
this.socket.emit('character:list', characters) this.socket.emit('character:list', characters)
} catch (error: any) { } catch (error: any) {

View File

@ -13,11 +13,22 @@ export default class AlertCommandEvent extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try { try {
// Check if command is alert if (!ChatService.isCommand(data.message, 'alert')) {
if (!ChatService.isCommand(data.message, 'alert')) return return
}
// Check if character exists // Check if character exists
if (!(await this.isCharacterGM())) return 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) const args = ChatService.getArgs('alert', data.message)

View File

@ -14,11 +14,22 @@ export default class SetTimeCommand extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try { try {
// Check if command is time if (!ChatService.isCommand(data.message, 'time')) {
if (!ChatService.isCommand(data.message, 'time')) return return
}
// Check if character exists and is GM // Check if character exists
if (!(await this.isCharacterGM())) return 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 // Get arguments
const args = ChatService.getArgs('time', data.message) const args = ChatService.getArgs('time', data.message)

View File

@ -1,7 +1,7 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import MapManager from '#managers/mapManager' import ZoneManager from '#managers/zoneManager'
import MapRepository from '#repositories/mapRepository' import ZoneRepository from '#repositories/zoneRepository'
import ChatService from '#services/chatService' import ChatService from '#services/chatService'
import TeleportService from '#services/teleportService' import TeleportService from '#services/teleportService'
@ -16,30 +16,36 @@ export default class TeleportCommandEvent extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: boolean) => void) { private async handleEvent(data: TypePayload, callback: (response: boolean) => void) {
try { try {
// Check if command is teleport 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 if (!ChatService.isCommand(data.message, 'teleport')) return
// Check if character exists and is GM
if (!(await this.isCharacterGM())) return
const character = await this.getCharacter()
if (!character) return
const args = ChatService.getArgs('teleport', data.message) const args = ChatService.getArgs('teleport', data.message)
if (!args || args.length === 0 || args.length > 3) { if (!args || args.length === 0 || args.length > 3) {
this.socket.emit('notification', { this.socket.emit('notification', {
title: 'Server message', title: 'Server message',
message: 'Usage: /teleport <mapId> [x] [y]' message: 'Usage: /teleport <zoneId> [x] [y]'
}) })
return return
} }
const mapId = args[0] as UUID const zoneId = args[0] as UUID
const targetX = args[1] ? parseInt(args[1], 10) : 0 const targetX = args[1] ? parseInt(args[1], 10) : 0
const targetY = args[2] ? parseInt(args[2], 10) : 0 const targetY = args[2] ? parseInt(args[2], 10) : 0
if (!mapId || isNaN(targetX) || isNaN(targetY)) { if (!zoneId || isNaN(targetX) || isNaN(targetY)) {
this.socket.emit('notification', { this.socket.emit('notification', {
title: 'Server message', title: 'Server message',
message: 'Invalid parameters. X and Y coordinates must be numbers.' message: 'Invalid parameters. X and Y coordinates must be numbers.'
@ -47,17 +53,16 @@ export default class TeleportCommandEvent extends BaseEvent {
return return
} }
const mapRepository = new MapRepository() const zone = await ZoneRepository.getById(zoneId)
const map = await mapRepository.getById(mapId) if (!zone) {
if (!map) {
this.socket.emit('notification', { this.socket.emit('notification', {
title: 'Server message', title: 'Server message',
message: 'Map not found' message: 'Zone not found'
}) })
return return
} }
if (character.map.id === map.id && targetX === character.positionX && targetY === character.positionY) { if (character.zone.id === zone.id && targetX === character.positionX && targetY === character.positionY) {
this.socket.emit('notification', { this.socket.emit('notification', {
title: 'Server message', title: 'Server message',
message: 'You are already at that location' message: 'You are already at that location'
@ -66,7 +71,7 @@ export default class TeleportCommandEvent extends BaseEvent {
} }
const success = await TeleportService.teleportCharacter(character.id, { const success = await TeleportService.teleportCharacter(character.id, {
targetMapId: map.id, targetZoneId: zone.id,
targetX, targetX,
targetY, targetY,
rotation: character.rotation rotation: character.rotation
@ -81,9 +86,9 @@ export default class TeleportCommandEvent extends BaseEvent {
this.socket.emit('notification', { this.socket.emit('notification', {
title: 'Server message', title: 'Server message',
message: `Teleported to ${map.name} (${targetX}, ${targetY})` message: `Teleported to ${zone.name} (${targetX}, ${targetY})`
}) })
this.logger.info('teleport', `Character ${character.id} teleported to map ${map.id} at position (${targetX}, ${targetY})`) this.logger.info('teleport', `Character ${character.id} teleported to zone ${zone.id} at position (${targetX}, ${targetY})`)
} catch (error: any) { } catch (error: any) {
this.logger.error(`Error in teleport command: ${error.message}`) this.logger.error(`Error in teleport command: ${error.message}`)
this.socket.emit('notification', { this.socket.emit('notification', {

View File

@ -1,5 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager' import WeatherManager from '#managers/weatherManager'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService' import ChatService from '#services/chatService'
type TypePayload = { type TypePayload = {
@ -13,16 +14,24 @@ export default class ToggleFogCommand extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try { try {
// Check if command is fog if (!ChatService.isCommand(data.message, 'fog')) {
if (!ChatService.isCommand(data.message, 'fog')) return return
}
// Check if character exists and is GM // Check if character exists
if (!(await this.isCharacterGM())) return const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
if (!character) {
this.logger.error('chat:alert_command error', 'Character not found')
return
}
const args = ChatService.getArgs('fog', data.message) // 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.setFogValue(args![0] ? Number(args![0]) : null); await WeatherManager.toggleFog()
callback(true)
} catch (error: any) { } catch (error: any) {
this.logger.error('command error', error.message) this.logger.error('command error', error.message)
callback(false) callback(false)

View File

@ -1,5 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager' import WeatherManager from '#managers/weatherManager'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService' import ChatService from '#services/chatService'
type TypePayload = { type TypePayload = {
@ -13,16 +14,24 @@ export default class ToggleRainCommand extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try { try {
// Check if command is rain if (!ChatService.isCommand(data.message, 'rain')) {
if (!ChatService.isCommand(data.message, 'rain')) return return
}
// Check if character exists and is GM // Check if character exists
if (!(await this.isCharacterGM())) return const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
if (!character) {
this.logger.error('chat:alert_command error', 'Character not found')
return
}
let args = ChatService.getArgs('rain', data.message) // 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.setRainValue(args![0] ? Number(args![0]) : null); await WeatherManager.toggleRain()
callback(true)
} catch (error: any) { } catch (error: any) {
this.logger.error('command error', error.message) this.logger.error('command error', error.message)
callback(false) callback(false)

View File

@ -1,6 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager' import ZoneManager from '#managers/zoneManager'
import MapRepository from '#repositories/mapRepository' import ZoneRepository from '#repositories/zoneRepository'
import ChatService from '#services/chatService' import ChatService from '#services/chatService'
type TypePayload = { type TypePayload = {
@ -18,22 +18,21 @@ export default class ChatMessageEvent extends BaseEvent {
return callback(false) return callback(false)
} }
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter) { if (!zoneCharacter) {
this.logger.error('chat:message error', 'Character not found') this.logger.error('chat:message error', 'Character not found')
return callback(false) return callback(false)
} }
const character = mapCharacter.character const character = zoneCharacter.character
const mapRepository = new MapRepository() const zone = await ZoneRepository.getById(character.zone.id)
const map = await mapRepository.getById(character.map.id) if (!zone) {
if (!map) { this.logger.error('chat:message error', 'Zone not found')
this.logger.error('chat:message error', 'Map not found')
return callback(false) return callback(false)
} }
if (await ChatService.sendMapMessage(character.getId(), map.getId(), data.message)) { if (await ChatService.sendZoneMessage(character.getId(), zone.getId(), data.message)) {
return callback(true) return callback(true)
} }

View File

@ -1,5 +1,5 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager' import ZoneManager from '#managers/zoneManager'
export default class DisconnectEvent extends BaseEvent { export default class DisconnectEvent extends BaseEvent {
public listen(): void { public listen(): void {
@ -15,13 +15,13 @@ export default class DisconnectEvent extends BaseEvent {
this.io.emit('user:disconnect', this.socket.userId) this.io.emit('user:disconnect', this.socket.userId)
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter) { if (!zoneCharacter) {
this.logger.info('User disconnected but had no character set') this.logger.info('User disconnected but had no character set')
return return
} }
await mapCharacter.disconnect(this.socket, this.io) await zoneCharacter.disconnect(this.socket, this.io)
this.logger.info('User disconnected along with their character') this.logger.info('User disconnected along with their character')
} catch (error: any) { } catch (error: any) {
this.logger.error('disconnect error: ' + error.message) this.logger.error('disconnect error: ' + error.message)

View File

@ -1,22 +1,28 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair' import { CharacterHair } from '#entities/characterHair'
import characterRepository from '#repositories/characterRepository'
export default class CharacterHairCreateEvent extends BaseEvent { export default class CharacterHairCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this)) this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const newCharacterHair = new CharacterHair() const newCharacterHair = new CharacterHair()
await newCharacterHair.setName('New hair').save() await newCharacterHair.setName('New hair').save()
return callback(true) callback(true, newCharacterHair)
} catch (error) { } catch (error) {
console.error('Error creating character hair:', error) console.error('Error creating character hair:', error)
return callback(false) callback(false)
} }
} }
} }

View File

@ -1,9 +1,9 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
interface IPayload { interface IPayload {
id: UUID id: number
} }
export default class characterHairDeleteEvent extends BaseEvent { export default class characterHairDeleteEvent extends BaseEvent {
@ -12,13 +12,20 @@ export default class characterHairDeleteEvent extends BaseEvent {
} }
private async handleEvent(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)
if (character.role !== 'gm') {
return callback(false)
}
try { try {
if (!(await this.isCharacterGM())) return
const characterHair = await CharacterHairRepository.getById(data.id) const characterHair = await CharacterHairRepository.getById(data.id)
await (await CharacterHairRepository.getById(data.id))?.delete() if (characterHair) {
await characterHair.delete()
}
return callback(true) callback(true)
} catch (error) { } catch (error) {
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false) callback(false)

View File

@ -1,6 +1,7 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair' import { CharacterHair } from '#entities/characterHair'
import CharacterHairRepository from '#repositories/characterHairRepository' import characterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
interface IPayload {} interface IPayload {}
@ -10,17 +11,19 @@ export default class characterHairListEvent extends BaseEvent {
} }
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
try { const character = await characterRepository.getById(this.socket.characterId as number)
if (!(await this.isCharacterGM())) return if (!character) {
this.logger.error('gm:characterHair:list error', 'Character not found')
const characterHairRepository = new CharacterHairRepository()
const items = await characterHairRepository.getAll()
await characterHairRepository.getEntityManager().populate(items, ['sprite'])
return callback(items)
} catch (error) {
this.logger.error('gm:characterHair:list error', error)
return callback([]) 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)
} }
} }

View File

@ -2,10 +2,11 @@ import { BaseEvent } from '#application/base/baseEvent'
import { CharacterGender } from '#application/enums' import { CharacterGender } from '#application/enums'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
type Payload = { type Payload = {
id: UUID id: number
name: string name: string
gender: CharacterGender gender: CharacterGender
isSelectable: boolean isSelectable: boolean
@ -18,18 +19,20 @@ export default class CharacterHairUpdateEvent extends BaseEvent {
} }
private async handleEvent(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)
if (character.role !== 'gm') {
return callback(false)
}
try { try {
if (!(await this.isCharacterGM())) return const sprite = await SpriteRepository.getById(data.spriteId)
const characterHair = await CharacterHairRepository.getById(data.id)
const spriteRepository = new SpriteRepository() if (characterHair) {
const sprite = await spriteRepository.getById(data.spriteId) await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update()
if (!sprite) return callback(false) }
const characterHairRepository = new CharacterHairRepository()
const characterHair = await characterHairRepository.getById(data.id)
if (!characterHair) return callback(false)
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite).setUpdatedAt(new Date()).save()
return callback(true) return callback(true)
} catch (error) { } catch (error) {

View File

@ -1,22 +1,41 @@
import { BaseEvent } from '#application/base/baseEvent' import { CharacterGender, CharacterRace } from '@prisma/client'
import { CharacterType } from '#entities/characterType' 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(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class CharacterTypeCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:create', this.handleEvent.bind(this)) this.socket.on('gm:characterType:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> { private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
const newCharacterType = new CharacterType() if (character.role !== 'gm') {
await newCharacterType.setName('New character type').save() return callback(false)
}
return callback(true, newCharacterType) const newCharacterType = await prisma.characterType.create({
data: {
name: 'New character type',
gender: CharacterGender.MALE,
race: CharacterRace.HUMAN
}
})
callback(true, newCharacterType)
} catch (error) { } catch (error) {
console.error('Error creating character type:', error) console.error('Error creating character type:', error)
return callback(false) callback(false)
} }
} }
} }

View File

@ -1,29 +1,41 @@
import { BaseEvent } from '#application/base/baseEvent' import { Server } from 'socket.io'
import { UUID } from '#application/types'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface IPayload { interface IPayload {
id: UUID id: number
} }
export default class CharacterTypeDeleteEvent extends BaseEvent { export default class CharacterTypeDeleteEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this)) this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
try { const character = await characterRepository.getById(this.socket.characterId!)
if (!(await this.isCharacterGM())) return if (!character) return callback(false)
const characterTypeRepository = new CharacterTypeRepository() if (character.role !== 'gm') {
const characterType = await characterTypeRepository.getById(data.id) return callback(false)
}
try {
const characterType = await CharacterTypeRepository.getById(data.id)
if (!characterType) return callback(false) if (!characterType) return callback(false)
await characterType.delete() await characterType.delete()
return callback(true) callback(true)
} catch (error) { } catch (error) {
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
return callback(false) callback(false)
} }
} }
} }

View File

@ -1,26 +1,37 @@
import { BaseEvent } from '#application/base/baseEvent' import { CharacterType } from '@prisma/client'
import { CharacterType } from '#entities/characterType' 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' import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface IPayload {} interface IPayload {}
export default class CharacterTypeListEvent extends BaseEvent { export default class CharacterTypeListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:list', this.handleEvent.bind(this)) this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
try { const character = await characterRepository.getById(this.socket.characterId as number)
if (!(await this.isCharacterGM())) return if (!character) {
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
const characterTypeRepository = new CharacterTypeRepository()
const items = await characterTypeRepository.getAll()
await characterTypeRepository.getEntityManager().populate(items, ['sprite'])
return callback(items)
} catch (error) {
this.logger.error('gm:characterType:list error', error)
return callback([]) return callback([])
} }
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`)
return callback([])
}
// get all objects
const items = await CharacterTypeRepository.getAll()
callback(items)
} }
} }

View File

@ -1,41 +1,53 @@
import { BaseEvent } from '#application/base/baseEvent' import { CharacterGender, CharacterRace } from '@prisma/client'
import { CharacterGender, CharacterRace } from '#application/enums' import { Server } from 'socket.io'
import { UUID } from '#application/types'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import prisma from '#application/prisma'
import SpriteRepository from '#repositories/spriteRepository' import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = { type Payload = {
id: UUID id: number
name: string name: string
gender: CharacterGender gender: CharacterGender
race: CharacterRace race: CharacterRace
isSelectable: boolean isSelectable: boolean
spriteId: UUID spriteId: string
} }
export default class CharacterTypeUpdateEvent extends BaseEvent { export default class CharacterTypeUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:update', this.handleEvent.bind(this)) this.socket.on('gm:characterType:update', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> { 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 { try {
if (!(await this.isCharacterGM())) return await prisma.characterType.update({
where: { id: data.id },
data: {
name: data.name,
gender: data.gender,
race: data.race,
isSelectable: data.isSelectable,
spriteId: data.spriteId
}
})
const characterTypeRepository = new CharacterTypeRepository() callback(true)
const characterType = await characterTypeRepository.getById(data.id)
if (!characterType) return callback(false)
const spriteRepository = new SpriteRepository()
const sprite = await spriteRepository.getById(data.spriteId)
if (!sprite) return callback(false)
await characterType.setName(data.name).setGender(data.gender).setRace(data.race).setIsSelectable(data.isSelectable).setSprite(sprite).save()
return callback(true)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
return callback(false) callback(false)
} }
} }
} }

View File

@ -1,22 +1,42 @@
import { BaseEvent } from '#application/base/baseEvent' import { Server } from 'socket.io'
import { Item } from '#entities/item'
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
) {}
export default class ItemCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:item:create', this.handleEvent.bind(this)) this.socket.on('gm:item:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> { private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
const newItem = new Item() if (character.role !== 'gm') {
await newItem.setName('New Item').setItemType('WEAPON').setStackable(false).setRarity('COMMON').setSprite(null).save() return callback(false)
}
return callback(true, newItem) const newItem = await prisma.item.create({
data: {
name: 'New Item',
itemType: 'WEAPON',
stackable: false,
rarity: 'COMMON',
spriteId: null
}
})
callback(true, newItem)
} catch (error) { } catch (error) {
console.error('Error creating item:', error) console.error('Error creating item:', error)
return callback(false) callback(false)
} }
} }
} }

View File

@ -1,30 +1,41 @@
import { BaseEvent } from '#application/base/baseEvent' import { Server } from 'socket.io'
import { UUID } from '#application/types'
import ItemRepository from '#repositories/itemRepository' import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
interface IPayload { interface IPayload {
id: UUID id: string
} }
export default class ItemDeleteEvent extends BaseEvent { export default class ItemDeleteEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:item:remove', this.handleEvent.bind(this)) this.socket.on('gm:item:remove', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
try { const character = await characterRepository.getById(this.socket.characterId as number)
if (!(await this.isCharacterGM())) return if (!character) return callback(false)
const itemRepository = new ItemRepository() if (character.role !== 'gm') {
const item = await itemRepository.getById(data.id)
if (!item) return callback(false)
await item.delete()
return callback(true)
} catch (error) {
this.logger.error(`Error deleting item ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
return callback(false) 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)
}
} }
} }

View File

@ -1,25 +1,37 @@
import { BaseEvent } from '#application/base/baseEvent' import { Item } from '@prisma/client'
import { Item } from '#entities/item' import { Server } from 'socket.io'
import ItemRepository from '#repositories/itemRepository'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import itemRepository from '#repositories/itemRepository'
interface IPayload {} interface IPayload {}
export default class ItemListEvent extends BaseEvent { export default class ItemListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:item:list', this.handleEvent.bind(this)) this.socket.on('gm:item:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> {
try { const character = await characterRepository.getById(this.socket.characterId as number)
if (!(await this.isCharacterGM())) return if (!character) {
gameMasterLogger.error('gm:item:list error', 'Character not found')
const itemRepository = new ItemRepository()
const items = await itemRepository.getAll()
return callback(items)
} catch (error) {
this.logger.error('gm:item:list error', error)
return callback([]) 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)
} }
} }

View File

@ -1,41 +1,55 @@
import { BaseEvent } from '#application/base/baseEvent' import { ItemType, ItemRarity } from '@prisma/client'
import { ItemType, ItemRarity } from '#application/enums' import { Server } from 'socket.io'
import { UUID } from '#application/types'
import ItemRepository from '#repositories/itemRepository' import { gameMasterLogger } from '#application/logger'
import SpriteRepository from '#repositories/spriteRepository' import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = { type Payload = {
id: UUID id: string
name: string name: string
description: string description: string | null
itemType: ItemType itemType: ItemType
stackable: boolean stackable: boolean
rarity: ItemRarity rarity: ItemRarity
spriteId: UUID spriteId: string | null
} }
export default class ItemUpdateEvent extends BaseEvent { export default class ItemUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:item:update', this.handleEvent.bind(this)) this.socket.on('gm:item:update', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> { 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 { try {
if (!(await this.isCharacterGM())) return await prisma.item.update({
where: { id: data.id },
const itemRepository = new ItemRepository() data: {
const item = await itemRepository.getById(data.id) name: data.name,
if (!item) return callback(false) description: data.description,
itemType: data.itemType,
const spriteRepository = new SpriteRepository() stackable: data.stackable,
const sprite = await spriteRepository.getById(data.spriteId) rarity: data.rarity,
if (!sprite) return callback(false) spriteId: data.spriteId
}
await item.setName(data.name).setDescription(data.description).setItemType(data.itemType).setStackable(data.stackable).setRarity(data.rarity).setSprite(sprite).save() })
return callback(true) return callback(true)
} catch (error) { } catch (error) {
console.error(error) gameMasterLogger.error(`Error updating item: ${error instanceof Error ? error.message : String(error)}`)
return callback(false) return callback(false)
} }
} }

View File

@ -1,26 +0,0 @@
import { BaseEvent } from '#application/base/baseEvent'
import { MapObject } from '#entities/mapObject'
import MapObjectRepository from '#repositories/mapObjectRepository'
interface IPayload {}
export default class MapObjectListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: MapObject[]) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
// Get all map objects
const mapObjectRepository = new MapObjectRepository()
const mapObjects = await mapObjectRepository.getAll()
return callback(mapObjects)
} catch (error) {
this.logger.error('gm:mapObject:list error', error)
return callback([])
}
}
}

View File

@ -1,39 +0,0 @@
import fs from 'fs'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { UUID } from '#application/types'
import MapObjectRepository from '#repositories/mapObjectRepository'
interface IPayload {
mapObjectId: UUID
}
export default class MapObjectRemoveEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
// remove the tile from the disk
const finalFilePath = Storage.getPublicPath('map_objects', data.mapObjectId + '.png')
fs.unlink(finalFilePath, async (err) => {
if (err) {
this.logger.error(`Error deleting object ${data.mapObjectId}: ${err.message}`)
callback(false)
return
}
const mapObjectRepository = new MapObjectRepository()
await (await mapObjectRepository.getById(data.mapObjectId))?.delete()
return callback(true)
})
} catch (error) {
this.logger.error(`Error deleting object ${data.mapObjectId}: ${error instanceof Error ? error.message : String(error)}`)
return callback(false)
}
}
}

View File

@ -1,46 +0,0 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import MapObjectRepository from '#repositories/mapObjectRepository'
type Payload = {
id: UUID
name: string
tags: string[]
originX: number
originY: number
frameRate: number
frameWidth: number
frameHeight: number
}
export default class MapObjectUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const mapObjectRepository = new MapObjectRepository()
const mapObject = await mapObjectRepository.getById(data.id)
if (!mapObject) return callback(false)
await mapObject
.setName(data.name)
.setTags(data.tags)
.setOriginX(data.originX)
.setOriginY(data.originY)
.setFrameRate(data.frameRate)
.setFrameWidth(data.frameWidth)
.setFrameHeight(data.frameHeight)
.save()
return callback(true)
} catch (error) {
console.error(error)
return callback(false)
}
}
}

View File

@ -1,54 +0,0 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage'
import { MapObject } from '#entities/mapObject'
interface IObjectData {
[key: string]: Buffer
}
export default class MapObjectUploadEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:upload', this.handleEvent.bind(this))
}
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const public_folder = Storage.getPublicPath('map_objects')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
const uploadPromises = Object.entries(data).map(async ([key, objectData]) => {
// Get image dimensions
const metadata = await sharp(objectData).metadata()
const width = metadata.width || 0
const height = metadata.height || 0
// Create new map object and save it to database
const mapObject = new MapObject()
await mapObject.setName('New map object').setTags([]).setOriginX(0).setOriginY(0).setFrameWidth(width).setFrameHeight(height).save()
// Save image to disk
const uuid = mapObject.getId()
const filename = `${uuid}.png`
const finalFilePath = Storage.getPublicPath('map_objects', filename)
await writeFile(finalFilePath, objectData)
this.logger.info('gm:mapObject:upload', `Object ${key} uploaded with id ${uuid}`)
})
await Promise.all(uploadPromises)
return callback(true)
} catch (error: any) {
this.logger.error('gm:mapObject:upload error', error.message)
return callback(false)
}
}
}

View File

@ -0,0 +1,32 @@
import { Object } from '@prisma/client'
import { Server } from 'socket.io'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import ObjectRepository from '#repositories/objectRepository'
interface IPayload {}
export default class ObjectListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object:list', this.handleEvent.bind(this))
}
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([])
if (character.role !== 'gm') {
return callback([])
}
// get all objects
const objects = await ObjectRepository.getAll()
callback(objects)
}
}

View File

@ -0,0 +1,59 @@
import fs from 'fs'
import { Server } from 'socket.io'
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
}
export default class ObjectRemoveEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object: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.object.delete({
where: {
id: data.object
}
})
// get root path
const public_folder = Storage.getPublicPath('objects')
// remove the tile from the disk
const finalFilePath = Storage.getPublicPath('objects', data.object + '.png')
fs.unlink(finalFilePath, (err) => {
if (err) {
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
callback(false)
return
}
callback(true)
})
} catch (error) {
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
}
}
}

View File

@ -0,0 +1,59 @@
import { Server } from 'socket.io'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: string
name: string
tags: string[]
originX: number
originY: number
isAnimated: boolean
frameRate: number
frameWidth: number
frameHeight: number
}
export default class ObjectUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object: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 object = await prisma.object.update({
where: {
id: data.id
},
data: {
name: data.name,
tags: data.tags,
originX: data.originX,
originY: data.originY,
isAnimated: data.isAnimated,
frameRate: data.frameRate,
frameWidth: data.frameWidth,
frameHeight: data.frameHeight
}
})
callback(true)
} catch (error) {
console.error(error)
callback(false)
}
}
}

View File

@ -0,0 +1,73 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
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
}
export default class ObjectUploadEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object:upload', this.handleEvent.bind(this))
}
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)
if (character.role !== 'gm') {
return callback(false)
}
const public_folder = Storage.getPublicPath('objects')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
const uploadPromises = Object.entries(data).map(async ([key, objectData]) => {
// Get image dimensions
const metadata = await sharp(objectData).metadata()
const width = metadata.width || 0
const height = metadata.height || 0
const object = await prisma.object.create({
data: {
name: key,
tags: [],
originX: 0,
originY: 0,
frameWidth: width,
frameHeight: height
}
})
const uuid = object.id
const filename = `${uuid}.png`
const finalFilePath = Storage.getPublicPath('objects', filename)
await writeFile(finalFilePath, objectData)
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)
})
await Promise.all(uploadPromises)
callback(true)
} catch (error: any) {
gameMasterLogger.error('gm:object:upload error', error.message)
callback(false)
}
}
}

View File

@ -1,39 +1,79 @@
import { BaseEvent } from '#application/base/baseEvent' import { Server } from 'socket.io'
import { UUID } from '#application/types'
import { Sprite } from '#entities/sprite' import type { Prisma } from '@prisma/client'
import SpriteRepository from '#repositories/spriteRepository'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
interface CopyPayload { interface CopyPayload {
id: UUID id: string
} }
export default class SpriteCopyEvent extends BaseEvent { export default class SpriteCopyEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:copy', this.handleEvent.bind(this)) this.socket.on('gm:sprite:copy', this.handleEvent.bind(this))
} }
private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> { private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return if (!(await this.validateGameMasterAccess())) {
const spriteRepository = new SpriteRepository()
const sourceSprite = await spriteRepository.getById(payload.id)
if (!sourceSprite) {
this.logger.error('gm:sprite:copy error', 'Source sprite not found')
return callback(false) return callback(false)
} }
// Populate source sprite with spriteActions const sourceSprite = await prisma.sprite.findUnique({
await spriteRepository.getEntityManager().populate(sourceSprite, ['spriteActions']) where: { id: payload.id },
include: {
spriteActions: true
}
})
const newSprite = new Sprite() if (!sourceSprite) {
await newSprite.setName(`${sourceSprite.getName()} (Copy)`).setSpriteActions(sourceSprite.getSpriteActions()).save() throw new Error('Source sprite not found')
}
return callback(true) 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) { } catch (error) {
this.logger.error(`Error copying sprite:`, String(error)) this.handleError(error, payload.id, callback)
return callback(false)
} }
} }
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)
}
} }

View File

@ -1,36 +1,51 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import { BaseEvent } from '#application/base/baseEvent' import { Server } from 'socket.io'
import Storage from '#application/storage'
import { Sprite } from '#entities/sprite' 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(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class SpriteCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:create', this.handleEvent.bind(this)) this.socket.on('gm:sprite:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const public_folder = Storage.getPublicPath('sprites') const public_folder = Storage.getPublicPath('sprites')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
const sprite = new Sprite() const sprite = await prisma.sprite.create({
await sprite.setName('New sprite').save() data: {
name: 'New sprite'
}
})
const uuid = sprite.id const uuid = sprite.id
// Create folder with uuid // Create folder with uuid
const sprite_folder = Storage.getPublicPath('sprites', uuid) const sprite_folder = Storage.getPublicPath('sprites', uuid)
await fs.mkdir(sprite_folder, { recursive: true }) await fs.mkdir(sprite_folder, { recursive: true })
return callback(true) callback(true)
} catch (error) { } catch (error) {
console.error('Error creating sprite:', error) console.error('Error creating sprite:', error)
return callback(false) callback(false)
} }
} }
} }

View File

@ -1,34 +1,47 @@
import fs from 'fs' import fs from 'fs'
import { BaseEvent } from '#application/base/baseEvent' import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage' import Storage from '#application/storage'
import { UUID } from '#application/types' import { TSocket } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository' import CharacterRepository from '#repositories/characterRepository'
type Payload = { type Payload = {
id: UUID id: string
} }
export default class GMSpriteDeleteEvent extends BaseEvent { export default class GMSpriteDeleteEvent {
private readonly public_folder: string
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = Storage.getPublicPath('sprites')
}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this)) this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
} }
private async handleEvent(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!)
if (!(await this.isCharacterGM())) return if (character?.role !== 'gm') {
await this.deleteSpriteFolder(data.id)
const spriteRepository = new SpriteRepository()
await (await spriteRepository.getById(data.id))?.delete()
this.logger.info(`Sprite ${data.id} deleted.`)
return callback(true)
} catch (error: any) {
this.logger.error('gm:sprite:delete error', error.message)
return callback(false) return callback(false)
} }
try {
await this.deleteSpriteFolder(data.id)
await this.deleteSpriteFromDatabase(data.id)
gameMasterLogger.info(`Sprite ${data.id} deleted.`)
callback(true)
} catch (error: any) {
gameMasterLogger.error('gm:sprite:delete error', error.message)
callback(false)
}
} }
private async deleteSpriteFolder(spriteId: string): Promise<void> { private async deleteSpriteFolder(spriteId: string): Promise<void> {
@ -38,4 +51,12 @@ export default class GMSpriteDeleteEvent extends BaseEvent {
await fs.promises.rmdir(finalFilePath, { recursive: true }) await fs.promises.rmdir(finalFilePath, { recursive: true })
} }
} }
private async deleteSpriteFromDatabase(spriteId: string): Promise<void> {
await prisma.sprite.delete({
where: {
id: spriteId
}
})
}
} }

View File

@ -1,27 +1,32 @@
import { BaseEvent } from '#application/base/baseEvent' import { Sprite } from '@prisma/client'
import { Sprite } from '#entities/sprite' import { Server } from 'socket.io'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
interface IPayload {} interface IPayload {}
export default class SpriteListEvent extends BaseEvent { export default class SpriteListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:list', this.handleEvent.bind(this)) this.socket.on('gm:sprite:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Sprite[]) => void): Promise<void> { private async handleEvent(data: any, callback: (response: Sprite[]) => void): Promise<void> {
try { const character = await characterRepository.getById(this.socket.characterId!)
if (!(await this.isCharacterGM())) return if (!character) return callback([])
// Get all sprites if (character.role !== 'gm') {
const spriteRepository = new SpriteRepository()
const sprites = await spriteRepository.getAll()
await spriteRepository.getEntityManager().populate(sprites, ['spriteActions'])
return callback(sprites)
} catch (error) {
this.logger.error('gm:sprite:list error', error)
return callback([]) return callback([])
} }
// get all sprites
const sprites = await SpriteRepository.getAll()
callback(sprites)
} }
} }

View File

@ -1,11 +1,15 @@
import { writeFile, mkdir } from 'node:fs/promises' import { writeFile, mkdir } from 'node:fs/promises'
import sharp from 'sharp' import sharp from 'sharp'
import { Server } from 'socket.io'
import { BaseEvent } from '#application/base/baseEvent' import type { Prisma, SpriteAction } from '@prisma/client'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage' import Storage from '#application/storage'
import { SpriteAction } from '#entities/spriteAction' import { TSocket } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository' import CharacterRepository from '#repositories/characterRepository'
// Constants // Constants
const ISOMETRIC_CONFIG = { const ISOMETRIC_CONFIG = {
@ -37,7 +41,7 @@ interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'fram
interface UpdatePayload { interface UpdatePayload {
id: string id: string
name: string name: string
spriteActions: SpriteAction[] spriteActions: Prisma.JsonValue
} }
interface ProcessedSpriteAction extends SpriteActionInput { interface ProcessedSpriteAction extends SpriteActionInput {
@ -58,14 +62,21 @@ interface SpriteAnalysis {
contentBounds: ContentBounds contentBounds: ContentBounds
} }
export default class SpriteUpdateEvent extends BaseEvent { export default class SpriteUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:sprite:update', this.handleEvent.bind(this)) this.socket.on('gm:sprite:update', this.handleEvent.bind(this))
} }
private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> { private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return if (!(await this.validateGameMasterAccess())) {
return callback(false)
}
const parsedActions = this.validateSpriteActions(payload.spriteActions) const parsedActions = this.validateSpriteActions(payload.spriteActions)
@ -100,6 +111,11 @@ export default class SpriteUpdateEvent extends BaseEvent {
} }
} }
private async validateGameMasterAccess(): Promise<boolean> {
const character = await CharacterRepository.getById(this.socket.characterId!)
return character?.role === 'gm'
}
private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] { private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] {
try { try {
const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[] const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[]
@ -359,8 +375,6 @@ export default class SpriteUpdateEvent extends BaseEvent {
} }
} }
}) })
await (await SpriteRepository.getById(id))?.setName(name).setSpriteActions(actions).update()
} }
private mapActionToDatabase(action: ProcessedSpriteAction) { private mapActionToDatabase(action: ProcessedSpriteAction) {
@ -369,6 +383,8 @@ export default class SpriteUpdateEvent extends BaseEvent {
sprites: action.sprites, sprites: action.sprites,
originX: action.originX, originX: action.originX,
originY: action.originY, originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameWidth: action.frameWidth, frameWidth: action.frameWidth,
frameHeight: action.frameHeight, frameHeight: action.frameHeight,
frameRate: action.frameRate frameRate: action.frameRate
@ -376,7 +392,7 @@ export default class SpriteUpdateEvent extends BaseEvent {
} }
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void { private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
this.logger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`) gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
callback(false) callback(false)
} }

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