Merge remote-tracking branch 'origin/main' into feature/#263

# Conflicts:
#	src/events/zone/characterMove.ts
This commit is contained in:
Dennis Postma 2025-01-03 17:30:40 +01:00
commit 0c155347c4
77 changed files with 1139 additions and 1208 deletions

View File

@ -33,8 +33,12 @@ 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. 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.
### 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,9 +1,25 @@
import { Migration } from '@mikro-orm/migrations'; import { Migration } from '@mikro-orm/migrations';
export class Migration20250101224501 extends Migration { export class Migration20250103003053 extends Migration {
override async up(): Promise<void> { override async up(): Promise<void> {
this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` int not null default 0, \`origin_y\` int not null default 0, \`is_animated\` tinyint(1) not null default false, \`frame_rate\` int not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`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, \`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, \`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 \`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;`);
@ -29,20 +45,16 @@ export class Migration20250101224501 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 \`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 \`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 \`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_zone_id_index\`(\`zone_id\`);`); 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_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, \`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(`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_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`chat\` add index \`chat_zone_id_index\`(\`zone_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(`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\`);`);
@ -52,19 +64,17 @@ export class Migration20250101224501 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 \`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(`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(`alter table \`zone_effect\` add index \`zone_effect_zone_id_index\`(\`zone_id\`);`);
this.addSql(`create table \`zone_event_tile\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`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 \`zone_event_tile\` add index \`zone_event_tile_zone_id_index\`(\`zone_id\`);`);
this.addSql(`create table \`zone_event_tile_teleport\` (\`id\` varchar(255) not null, \`zone_event_tile_id\` varchar(255) not null, \`to_zone_id\` varchar(255) not null, \`to_rotation\` int not null, \`to_position_x\` int not null, \`to_position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`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 \`zone_event_tile_teleport\` add unique \`zone_event_tile_teleport_zone_event_tile_id_unique\`(\`zone_event_tile_id\`);`);
this.addSql(`alter table \`zone_event_tile_teleport\` add index \`zone_event_tile_teleport_to_zone_id_index\`(\`to_zone_id\`);`);
this.addSql(`create table \`zone_object\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`depth\` int not null default 0, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`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 \`zone_object\` add index \`zone_object_zone_id_index\`(\`zone_id\`);`); 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_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;`);
@ -77,28 +87,18 @@ export class Migration20250101224501 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_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update 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_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_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`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_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

@ -17,6 +17,7 @@ export default defineConfig({
password: serverConfig.DB_PASS, password: serverConfig.DB_PASS,
dbName: serverConfig.DB_NAME, dbName: serverConfig.DB_NAME,
debug: serverConfig.ENV !== 'production', debug: serverConfig.ENV !== 'production',
// allowGlobalContext: true,
driverOptions: { driverOptions: {
allowPublicKeyRetrieval: true allowPublicKeyRetrieval: true
}, },

13
package-lock.json generated
View File

@ -1,5 +1,5 @@
{ {
"name": "nq-server", "name": "noxious-server",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@ -3179,15 +3179,16 @@
} }
}, },
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.4", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2", "has-tostringtag": "^1.0.2",
"hasown": "^2.0.1" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"

Binary file not shown.

View File

@ -6,13 +6,11 @@ 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 {
Database.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}`)
@ -20,18 +18,8 @@ class Database {
} }
} }
public static getORM(): MikroORM {
if (!Database.orm) {
throw new Error('Database not initialized. Call Database.initialize() first.')
}
return Database.orm
}
public static getEntityManager(): EntityManager { public static getEntityManager(): EntityManager {
if (!Database.em) { return Database.orm.em.fork()
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 ZoneEventTileType { export enum MapEventTileType {
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 { ZoneEventTile } from '#entities/zoneEventTile' import { MapEventTile } from '#entities/mapEventTile'
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport' import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
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 ZoneEventTileWithTeleport = ZoneEventTile & { export type MapEventTileWithTeleport = MapEventTile & {
teleport: ZoneEventTileTeleport teleport: MapEventTileTeleport
} }
export type AssetData = { export type AssetData = {

View File

@ -14,11 +14,11 @@ 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 { Map } from '#entities/map'
import { ZoneEffect } from '#entities/zoneEffect' import { MapEffect } from '#entities/mapEffect'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
// @TODO : Replace this with seeding // @TODO : Replace this with seeding
// https://mikro-orm.io/docs/seeding // https://mikro-orm.io/docs/seeding
@ -27,13 +27,13 @@ export default class InitCommand extends BaseCommand {
public async execute(): Promise<void> { public async execute(): Promise<void> {
// Assets // Assets
await this.importTiles() await this.importTiles()
await this.importObjects() await this.importMapObjects()
await this.createCharacterType() await this.createCharacterType()
await this.createCharacterHair() await this.createCharacterHair()
// await this.createCharacterEquipment() // await this.createCharacterEquipment()
// Zone // Map
await this.createZone() await this.createMap()
// User // User
await this.createUser() await this.createUser()
@ -51,19 +51,19 @@ export default class InitCommand extends BaseCommand {
} }
} }
private async importObjects(): Promise<void> { private async importMapObjects(): Promise<void> {
for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) { for (const mapObject of fs.readdirSync(Storage.getPublicPath('map_objects'))) {
const newMapObject = new MapObject() const newMapObject = new MapObject()
newMapObject newMapObject
.setId(object.split('.')[0] as UUID) .setId(mapObject.split('.')[0] as UUID)
.setName('New object') .setName('New map object')
.setFrameWidth( .setFrameWidth(
(await sharp(Storage.getPublicPath('objects', object)) (await sharp(Storage.getPublicPath('map_objects', mapObject))
.metadata() .metadata()
.then((metadata) => metadata.height)) ?? 0 .then((metadata) => metadata.height)) ?? 0
) )
.setFrameHeight( .setFrameHeight(
(await sharp(Storage.getPublicPath('objects', object)) (await sharp(Storage.getPublicPath('map_objects', mapObject))
.metadata() .metadata()
.then((metadata) => metadata.width)) ?? 0 .then((metadata) => metadata.width)) ?? 0
) )
@ -224,17 +224,17 @@ export default class InitCommand extends BaseCommand {
await equipmentSprite.save() await equipmentSprite.save()
} }
private async createZone(): Promise<void> { private async createMap(): Promise<void> {
const zone = new Zone() const map = new Map()
await zone await map
.setName('New zone') .setName('New map')
.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 ZoneEffect() const effect = new MapEffect()
await effect.setEffect('light').setStrength(100).setZone(zone).save() await effect.setEffect('light').setStrength(100).setMap(map).save()
} }
private async createUser(): Promise<void> { private async createUser(): Promise<void> {
@ -247,7 +247,7 @@ export default class InitCommand extends BaseCommand {
.setUser(user) .setUser(user)
.setName('root') .setName('root')
.setRole('gm') .setRole('gm')
.setZone((await ZoneRepository.getFirst())!) .setMap((await MapRepository.getFirst())!)
.setCharacterType((await CharacterTypeRepository.getFirst()) ?? undefined) .setCharacterType((await CharacterTypeRepository.getFirst()) ?? undefined)
.setCharacterHair((await CharacterHairRepository.getFirst()) ?? undefined) .setCharacterHair((await CharacterHairRepository.getFirst()) ?? undefined)
.save() .save()

View File

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

View File

@ -8,7 +8,7 @@ import { CharacterItem } from './characterItem'
import { CharacterType } from './characterType' import { CharacterType } from './characterType'
import { Chat } from './chat' import { Chat } from './chat'
import { User } from './user' import { User } from './user'
import { Zone } from './zone' import { Map } from './map'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@ -35,7 +35,7 @@ export class Character extends BaseEntity {
// Position // Position
@ManyToOne() @ManyToOne()
zone!: Zone // @TODO: Update to spawn point when current zone is not found map!: Map // @TODO: Update to spawn point when current map is not found
@Property() @Property()
positionX = 0 positionX = 0
@ -142,13 +142,13 @@ export class Character extends BaseEntity {
return this.chats return this.chats
} }
setZone(zone: Zone) { setMap(map: Map) {
this.zone = zone this.map = map
return this return this
} }
getZone() { getMap() {
return this.zone return this.map
} }
setPositionX(positionX: number) { setPositionX(positionX: number) {

View File

@ -26,10 +26,7 @@ export class CharacterType extends BaseEntity {
@Property() @Property()
isSelectable = false isSelectable = false
@OneToMany(() => Character, (character) => character.characterType) @ManyToOne({ nullable: true })
characters = new Collection<Character>(this)
@ManyToOne(() => Sprite, { nullable: true })
sprite?: Sprite sprite?: Sprite
@Property() @Property()
@ -109,13 +106,4 @@ export class CharacterType extends BaseEntity {
getUpdatedAt() { getUpdatedAt() {
return this.updatedAt return this.updatedAt
} }
setCharacters(characters: Collection<Character>) {
this.characters = characters
return this
}
getCharacters() {
return this.characters
}
} }

View File

@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Character } from './character' import { Character } from './character'
import { Zone } from './zone' import { Map } from './map'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@ -17,7 +17,7 @@ export class Chat extends BaseEntity {
character!: Character character!: Character
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
zone!: Zone map!: Map
@Property() @Property()
message!: string message!: string
@ -43,13 +43,13 @@ export class Chat extends BaseEntity {
return this.character return this.character
} }
setZone(zone: Zone) { setMap(map: Map) {
this.zone = zone this.map = map
return this return this
} }
getZone() { getMap() {
return this.zone return this.map
} }
setMessage(message: string) { setMessage(message: string) {

View File

@ -4,16 +4,16 @@ import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/
import { Character } from './character' import { Character } from './character'
import { Chat } from './chat' import { Chat } from './chat'
import { ZoneEffect } from './zoneEffect' import { MapEffect } from './mapEffect'
import { ZoneEventTile } from './zoneEventTile' import { MapEventTile } from './mapEventTile'
import { ZoneEventTileTeleport } from './zoneEventTileTeleport' import { MapEventTileTeleport } from './mapEventTileTeleport'
import { ZoneObject } from './zoneObject'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { PlacedMapObject } from '#entities/placedMapObject'
@Entity() @Entity()
export class Zone extends BaseEntity { export class Map extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ -38,22 +38,22 @@ export class Zone extends BaseEntity {
@Property() @Property()
updatedAt = new Date() updatedAt = new Date()
@OneToMany(() => ZoneEffect, (effect) => effect.zone) @OneToMany(() => MapEffect, (effect) => effect.map)
zoneEffects = new Collection<ZoneEffect>(this) mapEffects = new Collection<MapEffect>(this)
@OneToMany(() => ZoneEventTile, (tile) => tile.zone) @OneToMany(() => MapEventTile, (tile) => tile.map)
zoneEventTiles = new Collection<ZoneEventTile>(this) mapEventTiles = new Collection<MapEventTile>(this)
@OneToMany(() => ZoneEventTileTeleport, (teleport) => teleport.toZone) @OneToMany(() => MapEventTileTeleport, (teleport) => teleport.toMap)
zoneEventTileTeleports = new Collection<ZoneEventTileTeleport>(this) mapEventTileTeleports = new Collection<MapEventTileTeleport>(this)
@OneToMany(() => ZoneObject, (object) => object.zone) @OneToMany(() => PlacedMapObject, (object) => object.map)
zoneObjects = new Collection<ZoneObject>(this) placedMapObjects = new Collection<PlacedMapObject>(this)
@OneToMany(() => Character, (character) => character.zone) @OneToMany(() => Character, (character) => character.map)
characters = new Collection<Character>(this) characters = new Collection<Character>(this)
@OneToMany(() => Chat, (chat) => chat.zone) @OneToMany(() => Chat, (chat) => chat.map)
chats = new Collection<Chat>(this) chats = new Collection<Chat>(this)
setId(id: UUID) { setId(id: UUID) {
@ -128,40 +128,40 @@ export class Zone extends BaseEntity {
return this.updatedAt return this.updatedAt
} }
setZoneEffects(zoneEffects: Collection<ZoneEffect>) { setMapEffects(mapEffects: Collection<MapEffect>) {
this.zoneEffects = zoneEffects this.mapEffects = mapEffects
return this return this
} }
getZoneEffects() { getMapEffects() {
return this.zoneEffects return this.mapEffects
} }
setZoneEventTiles(zoneEventTiles: Collection<ZoneEventTile>) { setMapEventTiles(mapEventTiles: Collection<MapEventTile>) {
this.zoneEventTiles = zoneEventTiles this.mapEventTiles = mapEventTiles
return this return this
} }
getZoneEventTiles() { getMapEventTiles() {
return this.zoneEventTiles return this.mapEventTiles
} }
setZoneEventTileTeleports(zoneEventTileTeleports: Collection<ZoneEventTileTeleport>) { setMapEventTileTeleports(mapEventTileTeleports: Collection<MapEventTileTeleport>) {
this.zoneEventTileTeleports = zoneEventTileTeleports this.mapEventTileTeleports = mapEventTileTeleports
return this return this
} }
getZoneEventTileTeleports() { getMapEventTileTeleports() {
return this.zoneEventTileTeleports return this.mapEventTileTeleports
} }
setZoneObjects(zoneObjects: Collection<ZoneObject>) { setPlacedMapObjects(placedMapObjects: Collection<PlacedMapObject>) {
this.zoneObjects = zoneObjects this.placedMapObjects = placedMapObjects
return this return this
} }
getZoneObjects() { getPlacedMapObjects() {
return this.zoneObjects return this.placedMapObjects
} }
setCharacters(characters: Collection<Character>) { setCharacters(characters: Collection<Character>) {

View File

@ -2,18 +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 { Map } from './map'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@Entity() @Entity()
export class ZoneEffect extends BaseEntity { export class MapEffect extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
zone!: Zone map!: Map
@Property() @Property()
effect!: string effect!: string
@ -30,13 +30,13 @@ export class ZoneEffect extends BaseEntity {
return this.id return this.id
} }
setZone(zone: Zone) { setMap(map: Map) {
this.zone = zone this.map = map
return this return this
} }
getZone() { getMap() {
return this.zone return this.map
} }
setEffect(effect: string) { setEffect(effect: string) {

View File

@ -2,23 +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 { Zone } from './zone' import { Map } from './map'
import { ZoneEventTileTeleport } from './zoneEventTileTeleport' import { MapEventTileTeleport } from './mapEventTileTeleport'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { ZoneEventTileType } from '#application/enums' import { MapEventTileType } from '#application/enums'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@Entity() @Entity()
export class ZoneEventTile extends BaseEntity { export class MapEventTile extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
zone!: Zone map!: Map
@Enum(() => ZoneEventTileType) @Enum(() => MapEventTileType)
type!: ZoneEventTileType type!: MapEventTileType
@Property() @Property()
positionX!: number positionX!: number
@ -26,8 +26,8 @@ export class ZoneEventTile extends BaseEntity {
@Property() @Property()
positionY!: number positionY!: number
@OneToOne(() => ZoneEventTileTeleport, (teleport) => teleport.zoneEventTile) @OneToOne(() => MapEventTileTeleport, (teleport) => teleport.mapEventTile)
teleport?: ZoneEventTileTeleport teleport?: MapEventTileTeleport
setId(id: UUID) { setId(id: UUID) {
this.id = id this.id = id
@ -38,16 +38,16 @@ export class ZoneEventTile extends BaseEntity {
return this.id return this.id
} }
setZone(zone: Zone) { setMap(map: Map) {
this.zone = zone this.map = map
return this return this
} }
getZone() { getMap() {
return this.zone return this.map
} }
setType(type: ZoneEventTileType) { setType(type: MapEventTileType) {
this.type = type this.type = type
return this return this
} }
@ -74,7 +74,7 @@ export class ZoneEventTile extends BaseEntity {
return this.positionY return this.positionY
} }
setTeleport(teleport: ZoneEventTileTeleport) { setTeleport(teleport: MapEventTileTeleport) {
this.teleport = teleport this.teleport = teleport
return this return this
} }

View File

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

View File

@ -1,8 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { ZoneObject } from './zoneObject'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@ -18,10 +16,10 @@ export class MapObject extends BaseEntity {
@Property({ type: 'json', nullable: true }) @Property({ type: 'json', nullable: true })
tags?: any tags?: any
@Property() @Property({ type: 'decimal', precision: 10, scale: 2 })
originX = 0 originX = 0
@Property() @Property({ type: 'decimal', precision: 10, scale: 2 })
originY = 0 originY = 0
@Property() @Property()

View File

@ -2,7 +2,7 @@ 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 { Map } from './map'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@ -10,12 +10,12 @@ import { MapObject } from '#entities/mapObject'
//@TODO : Rename mapObject //@TODO : Rename mapObject
@Entity() @Entity()
export class ZoneObject extends BaseEntity { export class PlacedMapObject extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
zone!: Zone map!: Map
@ManyToOne({ deleteRule: 'cascade' }) @ManyToOne({ deleteRule: 'cascade' })
mapObject!: MapObject mapObject!: MapObject
@ -41,13 +41,13 @@ export class ZoneObject extends BaseEntity {
return this.id return this.id
} }
setZone(zone: Zone) { setMap(map: Map) {
this.zone = zone this.map = map
return this return this
} }
getZone() { getMap() {
return this.zone return this.map
} }
setMapObject(mapObject: MapObject) { setMapObject(mapObject: MapObject) {

View File

@ -11,8 +11,13 @@ 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 items: CharacterHair[] = await characterHairRepository.getAllSelectable() const items: CharacterHair[] = await characterHairRepository.getAllSelectable()
await Database.getEntityManager().populate(items, ['sprite']) await Database.getEntityManager().populate(items, ['sprite'])
callback(items) return callback(items)
} catch (error) {
this.logger.error('character:hair:list error', error)
return callback([])
}
} }
} }

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 ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
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'
@ -15,30 +15,14 @@ export default class CharacterConnectEvent extends BaseEvent {
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 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')
@ -57,11 +41,11 @@ export default class CharacterConnectEvent extends BaseEvent {
// Emit character connect event // Emit character connect event
callback({ character }) callback({ character })
// wait 300 ms, @TODO: Find a better way to do this // wait 300 ms, @TODO: Find a better way to do this, race condition
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 500))
await TeleportService.teleportCharacter(character.id, { await TeleportService.teleportCharacter(character.id, {
targetZoneId: character.zone.id, targetMapId: character.map.id,
targetX: character.positionX, targetX: character.positionX,
targetY: character.positionY, targetY: character.positionY,
rotation: character.rotation, rotation: character.rotation,
@ -75,6 +59,6 @@ export default class CharacterConnectEvent extends BaseEvent {
private async checkForActiveCharacters(): Promise<boolean> { private async checkForActiveCharacters(): Promise<boolean> {
const characters = await CharacterRepository.getByUserId(this.socket.userId!) const characters = await CharacterRepository.getByUserId(this.socket.userId!)
return characters?.some((char) => ZoneManager.getCharacterById(char.id)) ?? false return characters?.some((char) => MapManager.getCharacterById(char.id)) ?? false
} }
} }

View File

@ -5,7 +5,7 @@ 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 UserRepository from '#repositories/userRepository' import UserRepository from '#repositories/userRepository'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
export default class CharacterCreateEvent extends BaseEvent { export default class CharacterCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
@ -37,10 +37,10 @@ export default class CharacterCreateEvent extends BaseEvent {
} }
// @TODO: Change to default location // @TODO: Change to default location
const zone = await ZoneRepository.getFirst() const map = await MapRepository.getFirst()
const newCharacter = new Character() const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setZone(zone!).save() await newCharacter.setName(data.name).setUser(user).setMap(map!).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,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 { Character } from '#entities/character' import { Character } from '#entities/character'
import { Zone } from '#entities/zone' import { Map } from '#entities/map'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
type TypePayload = { type TypePayload = {
@ -9,7 +9,6 @@ type TypePayload = {
} }
type TypeResponse = { type TypeResponse = {
zone: Zone
characters: Character[] characters: Character[]
} }
@ -20,11 +19,7 @@ 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 character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId) await (await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId))?.delete()
if (character) {
await character.delete()
}
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
this.socket.emit('character:list', characters) this.socket.emit('character:list', characters)

View File

@ -1,5 +1,4 @@
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'
@ -10,9 +9,7 @@ export default class CharacterListEvent extends BaseEvent {
private async handleEvent(data: any): Promise<void> { private async handleEvent(data: any): Promise<void> {
try { try {
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) let characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!, ['characterType', 'characterHair'])
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) {
this.logger.error('character:list error', error.message) this.logger.error('character:list error', error.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 ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
import ChatService from '#services/chatService' import ChatService from '#services/chatService'
import TeleportService from '#services/teleportService' import TeleportService from '#services/teleportService'
@ -16,13 +16,13 @@ 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 {
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!zoneCharacter) { if (!mapCharacter) {
this.logger.error('chat:message error', 'Character not found') this.logger.error('chat:message error', 'Character not found')
return return
} }
const character = zoneCharacter.character const character = mapCharacter.character
if (character.role !== 'gm') { if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to set time but is not a game master.`) this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
@ -36,16 +36,16 @@ export default class TeleportCommandEvent extends BaseEvent {
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 <zoneId> [x] [y]' message: 'Usage: /teleport <mapId> [x] [y]'
}) })
return return
} }
const zoneId = args[0] as UUID const mapId = 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 (!zoneId || isNaN(targetX) || isNaN(targetY)) { if (!mapId || 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.'
@ -53,16 +53,16 @@ export default class TeleportCommandEvent extends BaseEvent {
return return
} }
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: 'Zone not found' message: 'Map not found'
}) })
return return
} }
if (character.zone.id === zone.id && targetX === character.positionX && targetY === character.positionY) { if (character.map.id === map.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'
@ -71,7 +71,7 @@ export default class TeleportCommandEvent extends BaseEvent {
} }
const success = await TeleportService.teleportCharacter(character.id, { const success = await TeleportService.teleportCharacter(character.id, {
targetZoneId: zone.id, targetMapId: map.id,
targetX, targetX,
targetY, targetY,
rotation: character.rotation rotation: character.rotation
@ -86,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 ${zone.name} (${targetX}, ${targetY})` message: `Teleported to ${map.name} (${targetX}, ${targetY})`
}) })
this.logger.info('teleport', `Character ${character.id} teleported to zone ${zone.id} at position (${targetX}, ${targetY})`) this.logger.info('teleport', `Character ${character.id} teleported to map ${map.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,6 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
import ChatService from '#services/chatService' import ChatService from '#services/chatService'
type TypePayload = { type TypePayload = {
@ -18,21 +18,21 @@ export default class ChatMessageEvent extends BaseEvent {
return callback(false) return callback(false)
} }
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!zoneCharacter) { if (!mapCharacter) {
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 = zoneCharacter.character const character = mapCharacter.character
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.sendZoneMessage(character.getId(), zone.getId(), data.message)) { if (await ChatService.sendMapMessage(character.getId(), map.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 ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
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 zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!zoneCharacter) { if (!mapCharacter) {
this.logger.info('User disconnected but had no character set') this.logger.info('User disconnected but had no character set')
return return
} }
await zoneCharacter.disconnect(this.socket, this.io) await mapCharacter.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,9 +1,9 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository' import { UUID } from '#application/types'
interface IPayload { interface IPayload {
id: number id: UUID
} }
export default class characterHairDeleteEvent extends BaseEvent { export default class characterHairDeleteEvent extends BaseEvent {
@ -12,20 +12,13 @@ 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 {
const characterHair = await CharacterHairRepository.getById(data.id) if (!(await this.isCharacterGM())) return
if (characterHair) {
await characterHair.delete()
}
callback(true) const characterHair = await CharacterHairRepository.getById(data.id)
await (await CharacterHairRepository.getById(data.id))?.delete()
return 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,7 +1,6 @@
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 {}
@ -11,19 +10,14 @@ 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> {
const character = await characterRepository.getById(this.socket.characterId as number) try {
if (!character) { if (!(await this.isCharacterGM())) return
this.logger.error('gm:characterHair:list error', 'Character not found')
return callback([])
}
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to list character hair but is not a game master.`)
return callback([])
}
// get all objects
const items = await characterHairRepository.getAll() const items = await characterHairRepository.getAll()
callback(items) return callback(items)
} catch (error) {
this.logger.error('gm:characterHair:list error', error)
return callback([])
}
} }
} }

View File

@ -6,7 +6,7 @@ import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
type Payload = { type Payload = {
id: number id: UUID
name: string name: string
gender: CharacterGender gender: CharacterGender
isSelectable: boolean isSelectable: boolean
@ -19,21 +19,17 @@ 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 sprite = await SpriteRepository.getById(data.spriteId)
const characterHair = await CharacterHairRepository.getById(data.id) const characterHair = await CharacterHairRepository.getById(data.id)
if (characterHair) { if (!characterHair) {
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update() return callback(false)
} }
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update()
return callback(true) return callback(true)
} catch (error) { } catch (error) {
this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)

View File

@ -1,41 +1,22 @@
import { CharacterGender, CharacterRace } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { Server } from 'socket.io' import { CharacterType } from '#entities/characterType'
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 {
const character = await characterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) return callback(false)
if (character.role !== 'gm') { const newCharacterType = new CharacterType()
return callback(false) await newCharacterType.setName('New character type').save()
}
const newCharacterType = await prisma.characterType.create({ return callback(true, newCharacterType)
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)
callback(false) return callback(false)
} }
} }
} }

View File

@ -1,41 +1,28 @@
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'
import { BaseEvent } from '#application/base/baseEvent'
interface IPayload { interface IPayload {
id: number id: UUID
} }
export default class CharacterTypeDeleteEvent { export default class CharacterTypeDeleteEvent extends BaseEvent {
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> {
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
const characterType = await CharacterTypeRepository.getById(data.id) const characterType = await CharacterTypeRepository.getById(data.id)
if (!characterType) return callback(false) if (!characterType) return callback(false)
await characterType.delete() await characterType.delete()
callback(true) return callback(true)
} catch (error) { } catch (error) {
gameMasterLogger.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) return callback(false)
} }
} }
} }

View File

@ -1,37 +1,23 @@
import { CharacterType } from '@prisma/client'
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'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterType } from '#entities/characterType'
interface IPayload {} interface IPayload {}
export default class CharacterTypeListEvent { export default class CharacterTypeListEvent extends BaseEvent {
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> {
const character = await characterRepository.getById(this.socket.characterId as number) try {
if (!character) { if (!(await this.isCharacterGM())) return
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
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() const items = await CharacterTypeRepository.getAll()
callback(items) return callback(items)
} catch (error) {
this.logger.error('gm:characterType:list error', error)
return callback([])
}
} }
} }

View File

@ -0,0 +1,19 @@
import ObjectRepository from '#repositories/mapObjectRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { MapObject } from '#entities/mapObject'
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> {
if (!(await this.isCharacterGM())) return
// get all objects
const objects = await ObjectRepository.getAll()
return callback(objects)
}
}

View File

@ -0,0 +1,38 @@
import fs from 'fs'
import Storage from '#application/storage'
import { BaseEvent } from '#application/base/baseEvent'
import MapObjectRepository from '#repositories/mapObjectRepository'
import { UUID } from '#application/types'
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> {
if (!(await this.isCharacterGM())) return
try {
// 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
}
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

@ -0,0 +1,46 @@
import { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import MapObjectRepository from '#repositories/mapObjectRepository'
type Payload = {
id: UUID
name: string
tags: string[]
originX: number
originY: number
isAnimated: boolean
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 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)
.setIsAnimated(data.isAnimated)
.setFrameRate(data.frameRate)
.setFrameWidth(data.frameWidth)
.setFrameHeight(data.frameHeight)
.update()
return callback(true)
} catch (error) {
console.error(error)
return callback(false)
}
}
}

View File

@ -0,0 +1,53 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
import Storage from '#application/storage'
import { BaseEvent } from '#application/base/baseEvent'
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(key).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

@ -1,32 +0,0 @@
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

@ -1,59 +0,0 @@
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

@ -1,59 +0,0 @@
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

@ -1,73 +0,0 @@
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,16 +1,10 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import { Server } from 'socket.io'
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import Storage from '#application/storage' import Storage from '#application/storage'
import { Sprite } from '#entities/sprite'
export default class SpriteCreateEvent extends BaseEvent { export default class SpriteCreateEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
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))
} }
@ -24,21 +18,19 @@ export default class SpriteCreateEvent extends BaseEvent {
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
const sprite = await prisma.sprite.create({ const sprite = new Sprite()
data: { await sprite.setName('New sprite').save()
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 })
callback(true) return callback(true)
} catch (error) { } catch (error) {
console.error('Error creating sprite:', error) console.error('Error creating sprite:', error)
callback(false) return callback(false)
} }
} }
} }

View File

@ -1,6 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { Zone } from '#entities/zone' import { Map } from '#entities/map'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
type Payload = { type Payload = {
name: string name: string
@ -8,30 +8,30 @@ type Payload = {
height: number height: number
} }
export default class ZoneCreateEvent extends BaseEvent { export default class MapCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:zone_editor:zone:create', this.handleEvent.bind(this)) this.socket.on('gm:map:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: Zone[]) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: Map[]) => void): Promise<void> {
try { try {
if (!(await this.isCharacterGM())) return if (!(await this.isCharacterGM())) return
this.logger.info(`User ${(await this.getCharacter())!.getId()} has created a new zone via zone editor.`) this.logger.info(`User ${(await this.getCharacter())!.getId()} has created a new map via map editor.`)
const zone = new Zone() const map = new Map()
await zone await map
.setName(data.name) .setName(data.name)
.setWidth(data.width) .setWidth(data.width)
.setHeight(data.height) .setHeight(data.height)
.setTiles(Array.from({ length: data.height }, () => Array.from({ length: data.width }, () => 'blank_tile'))) .setTiles(Array.from({ length: data.height }, () => Array.from({ length: data.width }, () => 'blank_tile')))
.save() .save()
const zoneList = await ZoneRepository.getAll() const mapList = await MapRepository.getAll()
return callback(zoneList) return callback(mapList)
} catch (error: any) { } catch (error: any) {
this.logger.error('gm:zone_editor:zone:create error', error.message) this.logger.error('gm:map:create error', error.message)
this.socket.emit('notification', { message: 'Failed to create zone.' }) this.socket.emit('notification', { message: 'Failed to create map.' })
return callback([]) return callback([])
} }
} }

View File

@ -0,0 +1,29 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import MapRepository from '#repositories/mapRepository'
type Payload = {
mapId: UUID
}
export default class MapDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:delete', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
if (!(await this.isCharacterGM())) return
try {
this.logger.info(`Deleting map ${data.mapId}`)
await (await MapRepository.getById(data.mapId))?.delete()
this.logger.info(`Map ${data.mapId} deleted successfully.`)
return callback(true)
} catch (error: unknown) {
this.logger.error('gm:map:delete error', error)
return callback(false)
}
}
}

View File

@ -0,0 +1,25 @@
import { BaseEvent } from '#application/base/baseEvent'
import { Map } from '#entities/map'
import MapRepository from '#repositories/mapRepository'
interface IPayload {}
export default class MapListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map[]) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
this.logger.info(`User ${(await this.getCharacter())!.getId()} has created a new map via map editor.`)
const maps = await MapRepository.getAll()
return callback(maps)
} catch (error: any) {
this.logger.error('gm:map:list error', error.message)
return callback([])
}
}
}

View File

@ -0,0 +1,44 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import MapRepository from '#repositories/mapRepository'
import Database from '#application/database'
interface IPayload {
mapId: UUID
}
export default class MapRequestEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:request', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
this.logger.info(`User ${(await this.getCharacter())!.getId()} has requested map via map editor.`)
if (!data.mapId) {
this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request map but did not provide a map id.`)
return callback(null)
}
const map = await MapRepository.getById(data.mapId)
if (!map) {
this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request map ${data.mapId} but it does not exist.`)
return callback(null)
}
console.log(map)
await Database.getEntityManager().populate(map, ['mapEventTiles', 'placedMapObjects'])
return callback(map)
} catch (error: any) {
this.logger.error('gm:map:request error', error.message)
return callback(null)
}
}
}

View File

@ -0,0 +1,130 @@
import { BaseEvent } from '#application/base/baseEvent'
import { MapEventTileType } from '#application/enums'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEffect } from '#entities/mapEffect'
import { MapEventTile } from '#entities/mapEventTile'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
import mapManager from '#managers/mapManager'
import MapRepository from '#repositories/mapRepository'
import { PlacedMapObject } from '#entities/placedMapObject'
interface IPayload {
mapId: UUID
name: string
width: number
height: number
tiles: string[][]
pvp: boolean
mapEventTiles: {
type: MapEventTileType
positionX: number
positionY: number
teleport?: {
toMapId: UUID
toPositionX: number
toPositionY: number
toRotation: number
}
}[]
mapEffects: {
effect: string
strength: number
}[]
placedMapObjects: PlacedMapObject[]
}
export default class MapUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:update', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const character = await this.getCharacter()
this.logger.info(`User ${character!.getId()} has updated map via map editor.`)
if (!data.mapId) {
this.logger.info(`User ${character!.getId()} tried to update map but did not provide a map id.`)
return callback(null)
}
let map = await MapRepository.getById(data.mapId)
if (!map) {
this.logger.info(`User ${character!.getId()} tried to update map ${data.mapId} but it does not exist.`)
return callback(null)
}
// Validation logic remains the same
if (data.tiles.length > data.height) {
data.tiles = data.tiles.slice(0, data.height)
}
for (let i = 0; i < data.tiles.length; i++) {
if (data.tiles[i].length > data.width) {
data.tiles[i] = data.tiles[i].slice(0, data.width)
}
}
data.mapEventTiles = data.mapEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
data.placedMapObjects = data.placedMapObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
// Clear existing collections
map.mapEventTiles.removeAll()
map.placedMapObjects.removeAll()
map.mapEffects.removeAll()
// Create and add new map event tiles
for (const tile of data.mapEventTiles) {
const mapEventTile = new MapEventTile().setType(tile.type).setPositionX(tile.positionX).setPositionY(tile.positionY).setMap(map)
if (tile.teleport) {
const teleport = new MapEventTileTeleport()
.setToMap((await MapRepository.getById(tile.teleport.toMapId))!)
.setToPositionX(tile.teleport.toPositionX)
.setToPositionY(tile.teleport.toPositionY)
.setToRotation(tile.teleport.toRotation)
mapEventTile.setTeleport(teleport)
}
map.mapEventTiles.add(mapEventTile)
}
// Create and add new map objects
for (const object of data.placedMapObjects) {
const mapObject = new PlacedMapObject().setMapObject(object.mapObject).setDepth(object.depth).setIsRotated(object.isRotated).setPositionX(object.positionX).setPositionY(object.positionY).setMap(map)
map.placedMapObjects.add(mapObject)
}
// Create and add new map effects
for (const effect of data.mapEffects) {
const mapEffect = new MapEffect().setEffect(effect.effect).setStrength(effect.strength).setMap(map)
map.mapEffects.add(mapEffect)
}
// Update map properties
await map.setName(data.name).setWidth(data.width).setHeight(data.height).setTiles(data.tiles).setPvp(data.pvp).setUpdatedAt(new Date()).update()
// Reload map from database to get fresh data
map = await MapRepository.getById(data.mapId)
if (!map) {
this.logger.info(`User ${character!.getId()} tried to update map ${data.mapId} but it does not exist after update.`)
return callback(null)
}
// Reload map for players
mapManager.unloadMap(data.mapId)
await mapManager.loadMap(map)
return callback(map)
} catch (error: any) {
this.logger.error(`gm:mapObject:update error: ${error instanceof Error ? error.message : String(error)}`)
return callback(null)
}
}
}

View File

@ -1,29 +0,0 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import ZoneRepository from '#repositories/zoneRepository'
type Payload = {
zoneId: UUID
}
export default class ZoneDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:zone_editor:zone:delete', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
if (!(await this.isCharacterGM())) return
try {
this.logger.info(`Deleting zone ${data.zoneId}`)
await (await ZoneRepository.getById(data.zoneId))?.delete()
this.logger.info(`Zone ${data.zoneId} deleted successfully.`)
return callback(true)
} catch (error: unknown) {
this.logger.error('gm:zone_editor:zone:delete error', error)
return callback(false)
}
}
}

View File

@ -1,25 +0,0 @@
import { BaseEvent } from '#application/base/baseEvent'
import { Zone } from '#entities/zone'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {}
export default class ZoneListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:zone_editor:zone:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Zone[]) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
this.logger.info(`User ${(await this.getCharacter())!.getId()} has created a new zone via zone editor.`)
const zones = await ZoneRepository.getAll()
return callback(zones)
} catch (error: any) {
this.logger.error('gm:zone_editor:zone:list error', error.message)
return callback([])
}
}
}

View File

@ -1,39 +0,0 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Zone } from '#entities/zone'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {
zoneId: UUID
}
export default class ZoneRequestEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:zone_editor:zone:request', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
this.logger.info(`User ${(await this.getCharacter())!.getId()} has requested zone via zone editor.`)
if (!data.zoneId) {
this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request zone but did not provide a zone id.`)
return callback(null)
}
const zone = await ZoneRepository.getById(data.zoneId)
if (!zone) {
this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request zone ${data.zoneId} but it does not exist.`)
return callback(null)
}
return callback(zone)
} catch (error: any) {
this.logger.error('gm:zone_editor:zone:request error', error.message)
return callback(null)
}
}
}

View File

@ -1,132 +0,0 @@
import { BaseEvent } from '#application/base/baseEvent'
import { ZoneEventTileType } from '#application/enums'
import { UUID } from '#application/types'
import { Zone } from '#entities/zone'
import { ZoneEffect } from '#entities/zoneEffect'
import { ZoneEventTile } from '#entities/zoneEventTile'
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
import { ZoneObject } from '#entities/zoneObject'
import zoneManager from '#managers/zoneManager'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {
zoneId: UUID
name: string
width: number
height: number
tiles: string[][]
pvp: boolean
zoneEventTiles: {
type: ZoneEventTileType
positionX: number
positionY: number
teleport?: {
toZoneId: UUID
toPositionX: number
toPositionY: number
toRotation: number
}
}[]
zoneEffects: {
effect: string
strength: number
}[]
zoneObjects: ZoneObject[]
}
export default class ZoneUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const character = await this.getCharacter()
this.logger.info(`User ${character!.getId()} has updated zone via zone editor.`)
if (!data.zoneId) {
this.logger.info(`User ${character!.getId()} tried to update zone but did not provide a zone id.`)
return callback(null)
}
let zone = await ZoneRepository.getById(data.zoneId)
if (!zone) {
this.logger.info(`User ${character!.getId()} tried to update zone ${data.zoneId} but it does not exist.`)
return callback(null)
}
// Validation logic remains the same
if (data.tiles.length > data.height) {
data.tiles = data.tiles.slice(0, data.height)
}
for (let i = 0; i < data.tiles.length; i++) {
if (data.tiles[i].length > data.width) {
data.tiles[i] = data.tiles[i].slice(0, data.width)
}
}
data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
// Clear existing collections
zone.zoneEventTiles.removeAll()
zone.zoneObjects.removeAll()
zone.zoneEffects.removeAll()
// Create and add new zone event tiles
for (const tile of data.zoneEventTiles) {
const zoneEventTile = new ZoneEventTile().setType(tile.type).setPositionX(tile.positionX).setPositionY(tile.positionY).setZone(zone)
if (tile.teleport) {
const teleport = new ZoneEventTileTeleport()
.setToZone((await ZoneRepository.getById(tile.teleport.toZoneId))!)
.setToPositionX(tile.teleport.toPositionX)
.setToPositionY(tile.teleport.toPositionY)
.setToRotation(tile.teleport.toRotation)
zoneEventTile.setTeleport(teleport)
}
zone.zoneEventTiles.add(zoneEventTile)
}
// Create and add new zone objects
for (const object of data.zoneObjects) {
const zoneObject = new ZoneObject().setMapObject(object.mapObject).setDepth(object.depth).setIsRotated(object.isRotated).setPositionX(object.positionX).setPositionY(object.positionY).setZone(zone)
zone.zoneObjects.add(zoneObject)
}
// Create and add new zone effects
for (const effect of data.zoneEffects) {
const zoneEffect = new ZoneEffect().setEffect(effect.effect).setStrength(effect.strength).setZone(zone)
zone.zoneEffects.add(zoneEffect)
}
// Update zone properties
await zone.setName(data.name).setWidth(data.width).setHeight(data.height).setTiles(data.tiles).setPvp(data.pvp).setUpdatedAt(new Date()).update()
// Reload zone from database to get fresh data
zone = await ZoneRepository.getById(data.zoneId)
if (!zone) {
this.logger.info(`User ${character!.getId()} tried to update zone ${data.zoneId} but it does not exist after update.`)
return callback(null)
}
// Reload zone for players
zoneManager.unloadZone(data.zoneId)
await zoneManager.loadZone(zone)
return callback(zone)
} catch (error: any) {
this.logger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
return callback(null)
}
}
}

View File

@ -0,0 +1,104 @@
import { BaseEvent } from '#application/base/baseEvent'
import { MapEventTileWithTeleport } from '#application/types'
import MapManager from '#managers/mapManager'
import MapCharacter from '#models/mapCharacter'
import mapEventTileRepository from '#repositories/mapEventTileRepository'
import CharacterService from '#services/characterService'
import MapEventTileService from '#services/mapEventTileService'
export default class CharacterMove extends BaseEvent {
private readonly characterService = CharacterService
private readonly mapEventTileService = MapEventTileService
public listen(): void {
this.socket.on('map:character:move', this.handleEvent.bind(this))
}
private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter?.character) {
this.logger.error('map:character:move error: Character not found or not initialized')
return
}
// If already moving, cancel current movement and wait for it to fully stop
if (mapCharacter.isMoving) {
mapCharacter.isMoving = false
await new Promise((resolve) => setTimeout(resolve, 100))
}
const path = await this.characterService.calculatePath(mapCharacter.character, positionX, positionY)
if (!path) {
this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'No valid path found')
return
}
// Start new movement
mapCharacter.isMoving = true
mapCharacter.currentPath = path // Add this property to MapCharacter class
await this.moveAlongPath(mapCharacter, path)
}
private async moveAlongPath(mapCharacter: MapCharacter, path: Array<{ x: number; y: number }>): Promise<void> {
const { character } = mapCharacter
for (let i = 0; i < path.length - 1; i++) {
if (!mapCharacter.isMoving || mapCharacter.currentPath !== path) {
return
}
const [start, end] = [path[i], path[i + 1]]
character.rotation = CharacterService.calculateRotation(start.x, start.y, end.x, end.y)
const mapEventTile = await mapEventTileRepository.getEventTileByMapIdAndPosition(character.map.id, Math.floor(end.x), Math.floor(end.y))
if (mapEventTile?.type === 'BLOCK') break
if (mapEventTile?.type === 'TELEPORT' && mapEventTile.teleport) {
await this.handleMapEventTile(mapEventTile as MapEventTileWithTeleport)
break
}
// Update position first
character.positionX = end.x
character.positionY = end.y
// Then emit with the same properties
this.io.in(character.map.id).emit('map:character:move', {
characterId: character.id,
positionX: character.positionX,
positionY: character.positionY,
rotation: character.rotation,
isMoving: true
})
await this.characterService.applyMovementDelay()
}
if (mapCharacter.isMoving && mapCharacter.currentPath === path) {
this.finalizeMovement(mapCharacter)
}
}
private async handleMapEventTile(mapEventTile: MapEventTileWithTeleport): Promise<void> {
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter) {
this.logger.error('map:character:move error: Character not found')
return
}
if (mapEventTile.teleport) {
await this.mapEventTileService.handleTeleport(this.io, this.socket, mapCharacter.character, mapEventTile.teleport)
}
}
private finalizeMovement(mapCharacter: MapCharacter): void {
mapCharacter.isMoving = false
this.io.in(mapCharacter.character.map.id).emit('map:character:move', {
characterId: mapCharacter.character.id,
positionX: mapCharacter.character.positionX,
positionY: mapCharacter.character.positionY,
rotation: mapCharacter.character.rotation,
isMoving: false
})
}
}

View File

@ -8,7 +8,7 @@ import Storage from '#application/storage'
import { AssetData, UUID } from '#application/types' import { AssetData, UUID } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
import TileRepository from '#repositories/tileRepository' import TileRepository from '#repositories/tileRepository'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
export class AssetsController extends BaseController { export class AssetsController extends BaseController {
/** /**
@ -28,24 +28,24 @@ export class AssetsController extends BaseController {
} }
/** /**
* List tiles by zone * List tiles by map
* @param req * @param req
* @param res * @param res
*/ */
public async listTilesByZone(req: Request, res: Response) { public async listTilesByMap(req: Request, res: Response) {
const zoneId = req.params.zoneId as UUID const mapId = req.params.mapId as UUID
if (!zoneId) { if (!mapId) {
return this.sendError(res, 'Invalid zone ID', 400) return this.sendError(res, 'Invalid map ID', 400)
} }
const zone = await ZoneRepository.getById(zoneId) const map = await MapRepository.getById(mapId)
if (!zone) { if (!map) {
return this.sendError(res, 'Zone not found', 404) return this.sendError(res, 'Map not found', 404)
} }
const assets: AssetData[] = [] const assets: AssetData[] = []
const tiles = await TileRepository.getByZoneId(zoneId) const tiles = await TileRepository.getByMapId(mapId)
for (const tile of tiles) { for (const tile of tiles) {
assets.push({ key: tile.getId(), data: '/assets/tiles/' + tile.getId() + '.png', group: 'tiles', updatedAt: tile.getUpdatedAt() } as AssetData) assets.push({ key: tile.getId(), data: '/assets/tiles/' + tile.getId() + '.png', group: 'tiles', updatedAt: tile.getUpdatedAt() } as AssetData)
@ -74,17 +74,17 @@ export class AssetsController extends BaseController {
await Database.getEntityManager().populate(sprite, ['spriteActions']) await Database.getEntityManager().populate(sprite, ['spriteActions'])
const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({ const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({
key: sprite.id + '-' + spriteAction.action, key: sprite.getId() + '-' + spriteAction.getAction(),
data: '/assets/sprites/' + sprite.getId() + '/' + spriteAction.getAction() + '.png', data: '/assets/sprites/' + sprite.getId() + '/' + spriteAction.getAction() + '.png',
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites', group: spriteAction.getIsAnimated() ? 'sprite_animations' : 'sprites',
updatedAt: sprite.getUpdatedAt(), updatedAt: sprite.getUpdatedAt(),
originX: Number(spriteAction.originX.toString()), originX: Number(spriteAction.getOriginX().toString()),
originY: Number(spriteAction.originY.toString()), originY: Number(spriteAction.getOriginY().toString()),
isAnimated: spriteAction.getIsAnimated(), isAnimated: spriteAction.getIsAnimated(),
frameRate: spriteAction.getFrameRate(), frameRate: spriteAction.getFrameRate(),
frameWidth: spriteAction.getFrameWidth(), frameWidth: spriteAction.getFrameWidth(),
frameHeight: spriteAction.getFrameHeight(), frameHeight: spriteAction.getFrameHeight(),
frameCount: JSON.parse(JSON.stringify(spriteAction.getSprites())).length frameCount: spriteAction.getSprites()?.length
})) }))
return this.sendSuccess(res, assets) return this.sendSuccess(res, assets)

View File

@ -33,7 +33,7 @@ class HttpManager {
// Assets routes // Assets routes
app.get('/assets/list_tiles', (req, res) => this.assetsController.listTiles(req, res)) app.get('/assets/list_tiles', (req, res) => this.assetsController.listTiles(req, res))
app.get('/assets/list_tiles/:zoneId', (req, res) => this.assetsController.listTilesByZone(req, res)) app.get('/assets/list_tiles/:mapId', (req, res) => this.assetsController.listTilesByMap(req, res))
app.get('/assets/list_sprite_actions/:spriteId', (req, res) => this.assetsController.listSpriteActions(req, res)) app.get('/assets/list_sprite_actions/:spriteId', (req, res) => this.assetsController.listSpriteActions(req, res))
app.get('/assets/:type/:spriteId?/:file', (req, res) => this.assetsController.downloadAsset(req, res)) app.get('/assets/:type/:spriteId?/:file', (req, res) => this.assetsController.downloadAsset(req, res))
} }

View File

@ -0,0 +1,50 @@
import Logger, { LoggerType } from '#application/logger'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import LoadedMap from '#models/loadedMap'
import MapCharacter from '#models/mapCharacter'
import MapRepository from '#repositories/mapRepository'
class MapManager {
private readonly maps: Record<UUID, LoadedMap> = {}
private logger = Logger.type(LoggerType.GAME)
public async boot(): Promise<void> {
const maps = await MapRepository.getAll()
await Promise.all(maps.map((map) => this.loadMap(map)))
this.logger.info(`Map manager loaded with ${Object.keys(this.maps).length} maps`)
}
public async loadMap(map: Map): Promise<void> {
this.maps[map.id] = new LoadedMap(map)
this.logger.info(`Map ID ${map.id} loaded`)
}
public unloadMap(mapId: UUID): void {
delete this.maps[mapId]
this.logger.info(`Map ID ${mapId} unloaded`)
}
public getLoadedMaps(): LoadedMap[] {
return Object.values(this.maps)
}
public getMapById(mapId: UUID): LoadedMap | undefined {
return this.maps[mapId]
}
public getCharacterById(characterId: UUID): MapCharacter | undefined {
for (const map of Object.values(this.maps)) {
const character = map.getCharactersInMap().find((char) => char.character.id === characterId)
if (character) return character
}
return undefined
}
public removeCharacter(characterId: UUID): void {
Object.values(this.maps).forEach((map) => map.removeCharacter(characterId))
}
}
export default new MapManager()

View File

@ -1,51 +0,0 @@
import Logger, { LoggerType } from '#application/logger'
import { UUID } from '#application/types'
import { Zone } from '#entities/zone'
import LoadedZone from '#models/loadedZone'
import ZoneCharacter from '#models/zoneCharacter'
import ZoneRepository from '#repositories/zoneRepository'
class ZoneManager {
private readonly zones = new Map<UUID, LoadedZone>()
private logger = Logger.type(LoggerType.GAME)
public async boot(): Promise<void> {
const zones = await ZoneRepository.getAll()
await Promise.all(zones.map((zone) => this.loadZone(zone)))
this.logger.info(`Zone manager loaded with ${this.zones.size} zones`)
}
public async loadZone(zone: Zone): Promise<void> {
const loadedZone = new LoadedZone(zone)
this.zones.set(zone.id, loadedZone)
this.logger.info(`Zone ID ${zone.id} loaded`)
}
public unloadZone(zoneId: UUID): void {
this.zones.delete(zoneId)
this.logger.info(`Zone ID ${zoneId} unloaded`)
}
public getLoadedZones(): LoadedZone[] {
return Array.from(this.zones.values())
}
public getZoneById(zoneId: UUID): LoadedZone | undefined {
return this.zones.get(zoneId)
}
public getCharacterById(characterId: UUID): ZoneCharacter | undefined {
for (const zone of this.zones.values()) {
const character = zone.getCharactersInZone().find((char) => char.character.id === characterId)
if (character) return character
}
return undefined
}
public removeCharacter(characterId: UUID): void {
this.zones.forEach((zone) => zone.removeCharacter(characterId))
}
}
export default new ZoneManager()

58
src/models/loadedMap.ts Normal file
View File

@ -0,0 +1,58 @@
import MapCharacter from './mapCharacter'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Map } from '#entities/map'
import mapEventTileRepository from '#repositories/mapEventTileRepository'
class LoadedMap {
private readonly map: Map
private characters: MapCharacter[] = []
constructor(map: Map) {
this.map = map
}
public getMap(): Map {
return this.map
}
public addCharacter(character: Character) {
const mapCharacter = new MapCharacter(character)
this.characters.push(mapCharacter)
}
public async removeCharacter(id: UUID) {
const mapCharacter = this.getCharacterById(id)
if (mapCharacter) {
await mapCharacter.savePosition()
this.characters = this.characters.filter((c) => c.character.id !== id)
}
}
public getCharacterById(id: UUID): MapCharacter | undefined {
return this.characters.find((c) => c.character.id === id)
}
public getCharactersInMap(): MapCharacter[] {
console.log(this.characters)
return this.characters
}
public async getGrid(): Promise<number[][]> {
let grid: number[][] = Array.from({ length: this.map.height }, () => Array.from({ length: this.map.width }, () => 0))
const eventTiles = await mapEventTileRepository.getAll(this.map.id)
// Set the grid values based on the event tiles, these are strings
eventTiles.forEach((eventTile) => {
if (eventTile.type === 'BLOCK') {
grid[eventTile.positionY][eventTile.positionX] = 1
}
})
return grid
}
}
export default LoadedMap

View File

@ -1,58 +0,0 @@
import ZoneCharacter from './zoneCharacter'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Zone } from '#entities/zone'
import zoneEventTileRepository from '#repositories/zoneEventTileRepository'
class LoadedZone {
private readonly zone: Zone
private characters: ZoneCharacter[] = []
constructor(zone: Zone) {
this.zone = zone
}
public getZone(): Zone {
return this.zone
}
public addCharacter(character: Character) {
const zoneCharacter = new ZoneCharacter(character)
this.characters.push(zoneCharacter)
}
public async removeCharacter(id: UUID) {
const zoneCharacter = this.getCharacterById(id)
if (zoneCharacter) {
await zoneCharacter.savePosition()
this.characters = this.characters.filter((c) => c.character.id !== id)
}
}
public getCharacterById(id: UUID): ZoneCharacter | undefined {
return this.characters.find((c) => c.character.id === id)
}
public getCharactersInZone(): ZoneCharacter[] {
console.log(this.characters)
return this.characters
}
public async getGrid(): Promise<number[][]> {
let grid: number[][] = Array.from({ length: this.zone.height }, () => Array.from({ length: this.zone.width }, () => 0))
const eventTiles = await zoneEventTileRepository.getAll(this.zone.id)
// Set the grid values based on the event tiles, these are strings
eventTiles.forEach((eventTile) => {
if (eventTile.type === 'BLOCK') {
grid[eventTile.positionY][eventTile.positionX] = 1
}
})
return grid
}
}
export default LoadedZone

View File

@ -3,10 +3,10 @@ import { Server } from 'socket.io'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import SocketManager from '#managers/socketManager' import SocketManager from '#managers/socketManager'
import ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
import TeleportService from '#services/teleportService' import TeleportService from '#services/teleportService'
class ZoneCharacter { class MapCharacter {
public readonly character: Character public readonly character: Character
public isMoving: boolean = false public isMoving: boolean = false
public currentPath: Array<{ x: number; y: number }> | null = null public currentPath: Array<{ x: number; y: number }> | null = null
@ -16,12 +16,12 @@ class ZoneCharacter {
} }
public async savePosition() { public async savePosition() {
await this.character.setPositionX(this.character.positionX).setPositionY(this.character.positionY).setRotation(this.character.rotation).setZone(this.character.zone).update() await this.character.setPositionX(this.character.positionX).setPositionY(this.character.positionY).setRotation(this.character.rotation).setMap(this.character.map).update()
} }
public async teleport(zoneId: number, targetX: number, targetY: number): Promise<void> { public async teleport(mapId: number, targetX: number, targetY: number): Promise<void> {
await TeleportService.teleportCharacter(this.character.id, { await TeleportService.teleportCharacter(this.character.id, {
targetZoneId: zoneId, targetMapId: mapId,
targetX, targetX,
targetY targetY
}) })
@ -34,13 +34,13 @@ class ZoneCharacter {
this.currentPath = null this.currentPath = null
await this.savePosition() await this.savePosition()
// Leave zone and remove from manager // Leave map and remove from manager
if (this.character.zone) { if (this.character.map) {
socket.leave(this.character.zone.id) socket.leave(this.character.map.id)
ZoneManager.removeCharacter(this.character.id) MapManager.removeCharacter(this.character.id)
// Notify zone players // Notify map players
io.in(this.character.zone.id).emit('zone:character:leave', this.character.id) io.in(this.character.map.id).emit('map:character:leave', this.character.id)
} }
// Notify all players // Notify all players
@ -51,4 +51,4 @@ class ZoneCharacter {
} }
} }
export default ZoneCharacter export default MapCharacter

View File

@ -1,12 +1,13 @@
import { BaseRepository } from '#application/base/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { LoadHint, Populate } from '@mikro-orm/core'
class CharacterRepository extends BaseRepository { class CharacterRepository extends BaseRepository {
async getByUserId(userId: UUID): Promise<Character[]> { async getByUserId(userId: UUID, populate?: LoadHint<Character, '*'>): Promise<Character[]> {
try { try {
const repository = this.em.getRepository(Character) const repository = this.em.getRepository(Character)
return await repository.find({ user: userId }) return await repository.find({ user: userId }, { populate: populate as Populate<Character> })
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`)
return [] return []

View File

@ -19,7 +19,7 @@ class CharacterTypeRepository extends BaseRepository {
return await repository.findAll() return await repository.findAll()
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`)
return null return []
} }
} }

View File

@ -35,12 +35,12 @@ class ChatRepository extends BaseRepository {
} }
} }
async getByZoneId(zoneId: UUID): Promise<Chat[]> { async getByMapId(mapId: UUID): Promise<Chat[]> {
try { try {
const repository = this.em.getRepository(Chat) const repository = this.em.getRepository(Chat)
return await repository.find({ zone: zoneId }) return await repository.find({ map: mapId })
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to get chats by zone ID: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Failed to get chats by map ID: ${error instanceof Error ? error.message : String(error)}`)
return [] return []
} }
} }

View File

@ -0,0 +1,33 @@
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { MapEventTile } from '#entities/mapEventTile'
class MapEventTileRepository extends BaseRepository {
async getAll(id: UUID): Promise<MapEventTile[]> {
try {
const repository = this.em.getRepository(MapEventTile)
return await repository.find({
map: id
})
} catch (error: any) {
this.logger.error(`Failed to get map event tiles: ${error.message}`)
return []
}
}
async getEventTileByMapIdAndPosition(mapId: UUID, positionX: number, positionY: number) {
try {
const repository = this.em.getRepository(MapEventTile)
return await repository.findOne({
map: mapId,
positionX: positionX,
positionY: positionY
})
} catch (error: any) {
this.logger.error(`Failed to get map event tile: ${error.message}`)
return null
}
}
}
export default new MapEventTileRepository()

View File

@ -0,0 +1,25 @@
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { MapObject } from '#entities/mapObject'
class MapObjectRepository extends BaseRepository {
async getById(id: UUID): Promise<MapObject | null> {
try {
const repository = this.em.getRepository(MapObject)
return await repository.findOne({ id })
} catch (error: any) {
return null
}
}
async getAll(): Promise<MapObject[]> {
try {
const repository = this.em.getRepository(MapObject)
return await repository.findAll()
} catch (error: any) {
return []
}
}
}
export default new MapObjectRepository()

View File

@ -0,0 +1,73 @@
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Map } from '#entities/map'
import { MapEventTile } from '#entities/mapEventTile'
import { MapObject } from '#entities/mapObject'
class MapRepository extends BaseRepository {
async getFirst(): Promise<Map | null> {
try {
const repository = this.em.getRepository(Map)
return await repository.findOne({ id: { $exists: true } })
} catch (error: any) {
this.logger.error(`Failed to get first map: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
async getAll(): Promise<Map[]> {
try {
const repository = this.em.getRepository(Map)
return await repository.findAll()
} catch (error: any) {
this.logger.error(`Failed to get all map: ${error.message}`)
return []
}
}
async getById(id: UUID) {
try {
const repository = this.em.getRepository(Map)
return await repository.findOne({ id })
} catch (error: any) {
this.logger.error(`Failed to get map by id: ${error.message}`)
return null
}
}
async getEventTiles(id: UUID): Promise<MapEventTile[]> {
try {
const repository = this.em.getRepository(MapEventTile)
return await repository.find({ map: id })
} catch (error: any) {
this.logger.error(`Failed to get map event tiles: ${error.message}`)
return []
}
}
async getFirstEventTile(mapId: UUID, positionX: number, positionY: number): Promise<MapEventTile | null> {
try {
const repository = this.em.getRepository(MapEventTile)
return await repository.findOne({
map: mapId,
positionX: positionX,
positionY: positionY
})
} catch (error: any) {
this.logger.error(`Failed to get map event tile: ${error.message}`)
return null
}
}
async getMapObjects(id: UUID): Promise<MapObject[]> {
try {
const repository = this.em.getRepository(MapObject)
return await repository.find({ map: id })
} catch (error: any) {
this.logger.error(`Failed to get map objects: ${error.message}`)
return []
}
}
}
export default new MapRepository()

View File

@ -1,24 +0,0 @@
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
class ObjectRepository extends BaseRepository {
async getById(id: UUID): Promise<any> {
try {
const repository = this.em.getRepository(Object)
return await repository.findOne({ id })
} catch (error: any) {
return null
}
}
async getAll(): Promise<any> {
try {
const repository = this.em.getRepository(Object)
return await repository.findAll()
} catch (error: any) {
return null
}
}
}
export default new ObjectRepository()

View File

@ -4,8 +4,8 @@ import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { unduplicateArray } from '#application/utilities' import { unduplicateArray } from '#application/utilities'
import { Tile } from '#entities/tile' import { Tile } from '#entities/tile'
import { Zone } from '#entities/zone' import { Map } from '#entities/map'
import ZoneService from '#services/zoneService' import MapService from '#services/mapService'
class TileRepository extends BaseRepository { class TileRepository extends BaseRepository {
async getById(id: UUID) { async getById(id: UUID) {
@ -37,18 +37,18 @@ class TileRepository extends BaseRepository {
} }
} }
async getByZoneId(zoneId: UUID) { async getByMapId(mapId: UUID) {
try { try {
const repository = this.em.getRepository(Zone) const repository = this.em.getRepository(Map)
const tileRepository = this.em.getRepository(Tile) const tileRepository = this.em.getRepository(Tile)
const zone = await repository.findOne({ id: zoneId }) const map = await repository.findOne({ id: mapId })
if (!zone) return [] if (!map) return []
const zoneTileArray = unduplicateArray(ZoneService.flattenZoneArray(JSON.parse(JSON.stringify(zone.tiles)))) const mapTileArray = unduplicateArray(MapService.flattenMapArray(JSON.parse(JSON.stringify(map.tiles))))
return await tileRepository.find({ return await tileRepository.find({
id: zoneTileArray id: mapTileArray
}) })
} catch (error: any) { } catch (error: any) {
return [] return []

View File

@ -1,33 +0,0 @@
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { ZoneEventTile } from '#entities/zoneEventTile'
class ZoneEventTileRepository extends BaseRepository {
async getAll(id: UUID): Promise<ZoneEventTile[]> {
try {
const repository = this.em.getRepository(ZoneEventTile)
return await repository.find({
zone: id
})
} catch (error: any) {
this.logger.error(`Failed to get zone event tiles: ${error.message}`)
return []
}
}
async getEventTileByZoneIdAndPosition(zoneId: UUID, positionX: number, positionY: number) {
try {
const repository = this.em.getRepository(ZoneEventTile)
return await repository.findOne({
zone: zoneId,
positionX: positionX,
positionY: positionY
})
} catch (error: any) {
this.logger.error(`Failed to get zone event tile: ${error.message}`)
return null
}
}
}
export default new ZoneEventTileRepository()

View File

@ -1,73 +0,0 @@
import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types'
import { Zone } from '#entities/zone'
import { ZoneEventTile } from '#entities/zoneEventTile'
import { ZoneObject } from '#entities/zoneObject'
class ZoneRepository extends BaseRepository {
async getFirst(): Promise<Zone | null> {
try {
const repository = this.em.getRepository(Zone)
return await repository.findOne({ id: { $exists: true } })
} catch (error: any) {
this.logger.error(`Failed to get first zone: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
async getAll(): Promise<Zone[]> {
try {
const repository = this.em.getRepository(Zone)
return await repository.findAll()
} catch (error: any) {
this.logger.error(`Failed to get all zone: ${error.message}`)
return []
}
}
async getById(id: UUID) {
try {
const repository = this.em.getRepository(Zone)
return await repository.findOne({ id })
} catch (error: any) {
this.logger.error(`Failed to get zone by id: ${error.message}`)
return null
}
}
async getEventTiles(id: UUID): Promise<ZoneEventTile[]> {
try {
const repository = this.em.getRepository(ZoneEventTile)
return await repository.find({ zone: id })
} catch (error: any) {
this.logger.error(`Failed to get zone event tiles: ${error.message}`)
return []
}
}
async getFirstEventTile(zoneId: UUID, positionX: number, positionY: number): Promise<ZoneEventTile | null> {
try {
const repository = this.em.getRepository(ZoneEventTile)
return await repository.findOne({
zone: zoneId,
positionX: positionX,
positionY: positionY
})
} catch (error: any) {
this.logger.error(`Failed to get zone event tile: ${error.message}`)
return null
}
}
async getZoneObjects(id: UUID): Promise<ZoneObject[]> {
try {
const repository = this.em.getRepository(ZoneObject)
return await repository.find({ zone: id })
} catch (error: any) {
this.logger.error(`Failed to get zone objects: ${error.message}`)
return []
}
}
}
export default new ZoneRepository()

View File

@ -13,7 +13,7 @@ import QueueManager from '#managers/queueManager'
import SocketManager from '#managers/socketManager' import SocketManager from '#managers/socketManager'
import UserManager from '#managers/userManager' import UserManager from '#managers/userManager'
import WeatherManager from '#managers/weatherManager' import WeatherManager from '#managers/weatherManager'
import ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
export class Server { export class Server {
private readonly app: Application private readonly app: Application
@ -45,7 +45,7 @@ export class Server {
UserManager.boot(), UserManager.boot(),
// DateManager.boot(), // DateManager.boot(),
// WeatherManager.boot(), // WeatherManager.boot(),
ZoneManager.boot(), MapManager.boot(),
ConsoleManager.boot() ConsoleManager.boot()
]) ])
} catch (error: any) { } catch (error: any) {

View File

@ -1,11 +1,11 @@
import { BaseService } from '#application/base/baseService' import { BaseService } from '#application/base/baseService'
import config from '#application/config' import config from '#application/config'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { Zone } from '#entities/zone' import { Map } from '#entities/map'
import SocketManager from '#managers/socketManager' import SocketManager from '#managers/socketManager'
import ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
type Position = { x: number; y: number } type Position = { x: number; y: number }
export type Node = Position & { parent?: Node; g: number; h: number; f: number } export type Node = Position & { parent?: Node; g: number; h: number; f: number }
@ -24,11 +24,11 @@ class CharacterService extends BaseService {
] ]
public async calculatePath(character: Character, targetX: number, targetY: number): Promise<Position[] | null> { public async calculatePath(character: Character, targetX: number, targetY: number): Promise<Position[] | null> {
const zone = ZoneManager.getZoneById(character.zone.id) const map = MapManager.getMapById(character.map.id)
const grid = await zone?.getGrid() const grid = await map?.getGrid()
if (!grid?.length) { if (!grid?.length) {
this.logger.error('zone:character:move error: Grid not found or empty') this.logger.error('map:character:move error: Grid not found or empty')
return null return null
} }

View File

@ -6,22 +6,22 @@ import { Chat } from '#entities/chat'
import SocketManager from '#managers/socketManager' import SocketManager from '#managers/socketManager'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
import ChatRepository from '#repositories/chatRepository' import ChatRepository from '#repositories/chatRepository'
import ZoneRepository from '#repositories/zoneRepository' import MapRepository from '#repositories/mapRepository'
class ChatService extends BaseService { class ChatService extends BaseService {
async sendZoneMessage(characterId: UUID, zoneId: UUID, message: string): Promise<boolean> { async sendMapMessage(characterId: UUID, mapId: UUID, message: string): Promise<boolean> {
try { try {
const character = await CharacterRepository.getById(characterId) const character = await CharacterRepository.getById(characterId)
if (!character) return false if (!character) return false
const zone = await ZoneRepository.getById(zoneId) const map = await MapRepository.getById(mapId)
if (!zone) return false if (!map) return false
const chat = new Chat() const chat = new Chat()
await chat.setCharacter(character).setZone(zone).setMessage(message).save() await chat.setCharacter(character).setMap(map).setMessage(message).save()
const io = SocketManager.getIO() const io = SocketManager.getIO()
io.to(zoneId).emit('chat:message', chat) io.to(mapId).emit('chat:message', chat)
return true return true
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to save chat message: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Failed to save chat message: ${error instanceof Error ? error.message : String(error)}`)

View File

@ -0,0 +1,49 @@
import { Server } from 'socket.io'
import { BaseService } from '#application/base/baseService'
import { ExtendedCharacter, TSocket } from '#application/types'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
import MapManager from '#managers/mapManager'
class MapEventTileService extends BaseService {
public async handleTeleport(io: Server, socket: TSocket, character: ExtendedCharacter, teleport: MapEventTileTeleport): Promise<void> {
if (teleport.toMap.id === character.map.id) return
const loadedMap = MapManager.getMapById(teleport.toMap.id)
if (!loadedMap) {
this.logger.error('map:character:join error: Loaded map not found')
return
}
const map = loadedMap.getMap()
const oldMapId = character.map.id
const newMapId = teleport.toMap.id
character.isMoving = false
// Update local character object
character.setMap(teleport.toMap).setRotation(teleport.toRotation).setPositionX(teleport.toPositionX).setPositionY(teleport.toPositionY)
await character.save()
// Remove and add character to new map
await loadedMap.removeCharacter(character.id)
loadedMap.addCharacter(character)
// Emit events
io.to(oldMapId).emit('map:character:leave', character.id)
io.to(newMapId).emit('map:character:join', character)
// Update socket rooms
socket.leave(oldMapId)
socket.join(newMapId)
// Send teleport information to the client
socket.emit('map:character:teleport', {
map,
characters: loadedMap.getCharactersInMap()
})
}
}
export default new MapEventTileService()

View File

@ -1,7 +1,7 @@
import { BaseService } from '#application/base/baseService' import { BaseService } from '#application/base/baseService'
class ZoneService extends BaseService { class MapService extends BaseService {
public flattenZoneArray(tiles: string[][]) { public flattenMapArray(tiles: string[][]) {
const normalArray = [] const normalArray = []
for (const row of tiles) { for (const row of tiles) {
@ -12,4 +12,4 @@ class ZoneService extends BaseService {
} }
} }
export default new ZoneService() export default new MapService()

View File

@ -2,11 +2,11 @@ import Logger, { LoggerType } from '#application/logger'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import SocketManager from '#managers/socketManager' import SocketManager from '#managers/socketManager'
import ZoneManager from '#managers/zoneManager' import MapManager from '#managers/mapManager'
import ZoneCharacter from '#models/zoneCharacter' import MapCharacter from '#models/mapCharacter'
interface TeleportOptions { interface TeleportOptions {
targetZoneId: UUID targetMapId: UUID
targetX: number targetX: number
targetY: number targetY: number
rotation?: number rotation?: number
@ -18,13 +18,13 @@ class TeleportService {
private readonly logger = Logger.type(LoggerType.GAME) private readonly logger = Logger.type(LoggerType.GAME)
public async teleportCharacter(characterId: UUID, options: TeleportOptions): Promise<boolean> { public async teleportCharacter(characterId: UUID, options: TeleportOptions): Promise<boolean> {
const { targetZoneId, targetX, targetY, rotation = 0, isInitialJoin = false, character } = options const { targetMapId, targetX, targetY, rotation = 0, isInitialJoin = false, character } = options
const socket = SocketManager.getSocketByCharacterId(characterId) const socket = SocketManager.getSocketByCharacterId(characterId)
const targetZone = ZoneManager.getZoneById(targetZoneId) const targetMap = MapManager.getMapById(targetMapId)
if (!socket || !targetZone) { if (!socket || !targetMap) {
this.logger.error(`Teleport failed - Missing socket or target zone for character ${characterId}`) this.logger.error(`Teleport failed - Missing socket or target map for character ${characterId}`)
return false return false
} }
@ -33,40 +33,40 @@ class TeleportService {
return false return false
} }
const existingCharacter = !isInitialJoin && ZoneManager.getCharacterById(characterId) const existingCharacter = !isInitialJoin && MapManager.getCharacterById(characterId)
const zoneCharacter = isInitialJoin const mapCharacter = isInitialJoin
? new ZoneCharacter(character!) ? new MapCharacter(character!)
: existingCharacter || : existingCharacter ||
(() => { (() => {
this.logger.error(`Teleport failed - Character ${characterId} not found in ZoneManager`) this.logger.error(`Teleport failed - Character ${characterId} not found in MapManager`)
return null return null
})() })()
if (!zoneCharacter) return false if (!mapCharacter) return false
try { try {
const currentZoneId = zoneCharacter.character.zone?.id const currentMapId = mapCharacter.character.map?.id
const io = SocketManager.getIO() const io = SocketManager.getIO()
// Handle current zone cleanup // Handle current map cleanup
if (currentZoneId) { if (currentMapId) {
socket.leave(currentZoneId) socket.leave(currentMapId)
ZoneManager.removeCharacter(characterId) MapManager.removeCharacter(characterId)
io.in(currentZoneId).emit('zone:character:leave', characterId) io.in(currentMapId).emit('map:character:leave', characterId)
} }
// Update character position and zone // Update character position and map
await zoneCharacter.character.setPositionX(targetX).setPositionY(targetY).setRotation(rotation).setZone(targetZone.getZone()).update() await mapCharacter.character.setPositionX(targetX).setPositionY(targetY).setRotation(rotation).setMap(targetMap.getMap()).update()
// Join new zone // Join new map
socket.join(targetZoneId) socket.join(targetMapId)
targetZone.addCharacter(zoneCharacter.character) targetMap.addCharacter(mapCharacter.character)
// Notify clients // Notify clients
io.in(targetZoneId).emit('zone:character:join', zoneCharacter) io.in(targetMapId).emit('map:character:join', mapCharacter)
socket.emit('zone:character:teleport', { socket.emit('map:character:teleport', {
zone: targetZone.getZone(), map: targetMap.getMap(),
characters: targetZone.getCharactersInZone() characters: targetMap.getCharactersInMap()
}) })
return true return true

View File

@ -1,49 +0,0 @@
import { Server } from 'socket.io'
import { BaseService } from '#application/base/baseService'
import { ExtendedCharacter, TSocket } from '#application/types'
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
import ZoneManager from '#managers/zoneManager'
class ZoneEventTileService extends BaseService {
public async handleTeleport(io: Server, socket: TSocket, character: ExtendedCharacter, teleport: ZoneEventTileTeleport): Promise<void> {
if (teleport.toZone.id === character.zone.id) return
const loadedZone = ZoneManager.getZoneById(teleport.toZone.id)
if (!loadedZone) {
this.logger.error('zone:character:join error: Loaded zone not found')
return
}
const zone = loadedZone.getZone()
const oldZoneId = character.zone.id
const newZoneId = teleport.toZone.id
character.isMoving = false
// Update local character object
character.setZone(teleport.toZone).setRotation(teleport.toRotation).setPositionX(teleport.toPositionX).setPositionY(teleport.toPositionY)
await character.save()
// Remove and add character to new zone
await loadedZone.removeCharacter(character.id)
loadedZone.addCharacter(character)
// Emit events
io.to(oldZoneId).emit('zone:character:leave', character.id)
io.to(newZoneId).emit('zone:character:join', character)
// Update socket rooms
socket.leave(oldZoneId)
socket.join(newZoneId)
// Send teleport information to the client
socket.emit('zone:character:teleport', {
zone,
characters: loadedZone.getCharactersInZone()
})
}
}
export default new ZoneEventTileService()