diff --git a/README.md b/README.md index 4657d72..91e7121 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,12 @@ Run `npx mikro-orm migration:create --initial` to create a new initial migration ### Create migrations -Run `npx mikro-orm migration:create` to create a new migration. +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 -Run `npx mikro-orm migration:up` to apply all pending migrations. \ No newline at end of file +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. \ No newline at end of file diff --git a/migrations/Migration20250101224501.ts b/migrations/Migration20250103003053.ts similarity index 63% rename from migrations/Migration20250101224501.ts rename to migrations/Migration20250103003053.ts index 2e55324..b555b9e 100644 --- a/migrations/Migration20250101224501.ts +++ b/migrations/Migration20250103003053.ts @@ -1,9 +1,25 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250101224501 extends Migration { +export class Migration20250103003053 extends Migration { override async up(): Promise { - 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;`); @@ -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 unique \`password_reset_token_token_unique\`(\`token\`);`); - this.addSql(`create table \`world\` (\`date\` datetime not null, \`is_rain_enabled\` tinyint(1) not null default false, \`rain_percentage\` int not null default 0, \`is_fog_enabled\` tinyint(1) not null default false, \`fog_density\` int not null default 0, primary key (\`date\`)) default character set utf8mb4 engine = InnoDB;`); - - this.addSql(`create table \`zone\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); - - this.addSql(`create table \`character\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`name\` varchar(255) not null, \`online\` tinyint(1) not null default false, \`role\` varchar(255) not null default 'player', \`zone_id\` varchar(255) not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`rotation\` int not null default 0, \`character_type_id\` varchar(255) null, \`character_hair_id\` varchar(255) null, \`alignment\` int not null default 50, \`hitpoints\` int not null default 100, \`mana\` int not null default 100, \`level\` int not null default 1, \`experience\` int not null default 0, \`strength\` int not null default 10, \`dexterity\` int not null default 10, \`intelligence\` int not null default 10, \`wisdom\` int not null default 10, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); + this.addSql(`create table \`character\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`name\` varchar(255) not null, \`online\` tinyint(1) not null default false, \`role\` varchar(255) not null default 'player', \`map_id\` varchar(255) not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`rotation\` int not null default 0, \`character_type_id\` varchar(255) null, \`character_hair_id\` varchar(255) null, \`alignment\` int not null default 50, \`hitpoints\` int not null default 100, \`mana\` int not null default 100, \`level\` int not null default 1, \`experience\` int not null default 0, \`strength\` int not null default 10, \`dexterity\` int not null default 10, \`intelligence\` int not null default 10, \`wisdom\` int not null default 10, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`alter table \`character\` add index \`character_user_id_index\`(\`user_id\`);`); this.addSql(`alter table \`character\` add unique \`character_name_unique\`(\`name\`);`); - this.addSql(`alter table \`character\` add index \`character_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_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_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(`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_item_id_index\`(\`character_item_id\`);`); - this.addSql(`create table \`zone_effect\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); - this.addSql(`alter table \`zone_effect\` add index \`zone_effect_zone_id_index\`(\`zone_id\`);`); + this.addSql(`create table \`world\` (\`date\` datetime not null, \`is_rain_enabled\` tinyint(1) not null default false, \`rain_percentage\` int not null default 0, \`is_fog_enabled\` tinyint(1) not null default false, \`fog_density\` int not null default 0, primary key (\`date\`)) default character set utf8mb4 engine = InnoDB;`); - this.addSql(`create table \`zone_event_tile\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); - this.addSql(`alter table \`zone_event_tile\` add index \`zone_event_tile_zone_id_index\`(\`zone_id\`);`); + this.addSql(`alter table \`map_effect\` add constraint \`map_effect_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); - this.addSql(`create table \`zone_event_tile_teleport\` (\`id\` varchar(255) not null, \`zone_event_tile_id\` varchar(255) not null, \`to_zone_id\` varchar(255) not null, \`to_rotation\` int not null, \`to_position_x\` int not null, \`to_position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); - this.addSql(`alter table \`zone_event_tile_teleport\` add unique \`zone_event_tile_teleport_zone_event_tile_id_unique\`(\`zone_event_tile_id\`);`); - this.addSql(`alter table \`zone_event_tile_teleport\` add index \`zone_event_tile_teleport_to_zone_id_index\`(\`to_zone_id\`);`); + this.addSql(`alter table \`map_event_tile\` add constraint \`map_event_tile_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); - this.addSql(`create table \`zone_object\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`depth\` int not null default 0, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); - this.addSql(`alter table \`zone_object\` add index \`zone_object_zone_id_index\`(\`zone_id\`);`); - this.addSql(`alter table \`zone_object\` add index \`zone_object_map_object_id_index\`(\`map_object_id\`);`); + this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_map_event_tile_id_foreign\` foreign key (\`map_event_tile_id\`) references \`map_event_tile\` (\`id\`) on update cascade on delete cascade;`); + this.addSql(`alter table \`map_event_tile_teleport\` add constraint \`map_event_tile_teleport_to_map_id_foreign\` foreign key (\`to_map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); + + this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); + this.addSql(`alter table \`placed_map_object\` add constraint \`placed_map_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`); @@ -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 \`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_hair_id_foreign\` foreign key (\`character_hair_id\`) references \`character_hair\` (\`id\`) on update cascade on delete set null;`); this.addSql(`alter table \`chat\` add constraint \`chat_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`); - this.addSql(`alter table \`chat\` add constraint \`chat_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`); + this.addSql(`alter table \`chat\` add constraint \`chat_map_id_foreign\` foreign key (\`map_id\`) references \`map\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`character_item\` add constraint \`character_item_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`character_item\` add constraint \`character_item_item_id_foreign\` foreign key (\`item_id\`) references \`item\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`character_equipment\` add constraint \`character_equipment_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`); this.addSql(`alter table \`character_equipment\` add constraint \`character_equipment_character_item_id_foreign\` foreign key (\`character_item_id\`) references \`character_item\` (\`id\`) on update cascade on delete cascade;`); - - 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;`); } } diff --git a/mikro-orm.config.ts b/mikro-orm.config.ts index 9f5ded4..5bc1e12 100644 --- a/mikro-orm.config.ts +++ b/mikro-orm.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ password: serverConfig.DB_PASS, dbName: serverConfig.DB_NAME, debug: serverConfig.ENV !== 'production', + // allowGlobalContext: true, driverOptions: { allowPublicKeyRetrieval: true }, diff --git a/package-lock.json b/package-lock.json index 8ab468a..15d744f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "nq-server", + "name": "noxious-server", "lockfileVersion": 3, "requires": true, "packages": { @@ -3179,15 +3179,16 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" diff --git a/public/assets.zip b/public/assets.zip index 0cdeddf..c6c406b 100644 Binary files a/public/assets.zip and b/public/assets.zip differ diff --git a/src/application/database.ts b/src/application/database.ts index 77994c2..536cbdf 100644 --- a/src/application/database.ts +++ b/src/application/database.ts @@ -6,13 +6,11 @@ import config from '../../mikro-orm.config' class Database { private static orm: MikroORM - private static em: EntityManager private static logger = Logger.type(LoggerType.APP) public static async initialize(): Promise { try { Database.orm = await MikroORM.init(config) - Database.em = Database.orm.em.fork() this.logger.info('Database connection initialized') } catch (error) { this.logger.error(`MikroORM connection failed: ${error}`) @@ -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 { - if (!Database.em) { - throw new Error('Database not initialized. Call Database.initialize() first.') - } - return Database.em + return Database.orm.em.fork() } } diff --git a/src/application/enums.ts b/src/application/enums.ts index d347a4c..b4609cc 100644 --- a/src/application/enums.ts +++ b/src/application/enums.ts @@ -55,7 +55,7 @@ export enum CharacterEquipmentSlotType { RING = 'RING' } -export enum ZoneEventTileType { +export enum MapEventTileType { BLOCK = 'BLOCK', TELEPORT = 'TELEPORT', NPC = 'NPC', diff --git a/src/application/types.ts b/src/application/types.ts index 5dda62a..f6aa12b 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -1,8 +1,8 @@ import { Server, Socket } from 'socket.io' import { Character } from '#entities/character' -import { ZoneEventTile } from '#entities/zoneEventTile' -import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport' +import { MapEventTile } from '#entities/mapEventTile' +import { MapEventTileTeleport } from '#entities/mapEventTileTeleport' export type UUID = `${string}-${string}-${string}-${string}-${string}` @@ -26,8 +26,8 @@ export type ExtendedCharacter = Character & { resetMovement?: boolean } -export type ZoneEventTileWithTeleport = ZoneEventTile & { - teleport: ZoneEventTileTeleport +export type MapEventTileWithTeleport = MapEventTile & { + teleport: MapEventTileTeleport } export type AssetData = { diff --git a/src/commands/init.ts b/src/commands/init.ts index 088d56d..1f414c7 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -14,11 +14,11 @@ import { Sprite } from '#entities/sprite' import { SpriteAction } from '#entities/spriteAction' import { Tile } from '#entities/tile' import { User } from '#entities/user' -import { Zone } from '#entities/zone' -import { ZoneEffect } from '#entities/zoneEffect' +import { Map } from '#entities/map' +import { MapEffect } from '#entities/mapEffect' import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository' -import ZoneRepository from '#repositories/zoneRepository' +import MapRepository from '#repositories/mapRepository' // @TODO : Replace this with seeding // https://mikro-orm.io/docs/seeding @@ -27,13 +27,13 @@ export default class InitCommand extends BaseCommand { public async execute(): Promise { // Assets await this.importTiles() - await this.importObjects() + await this.importMapObjects() await this.createCharacterType() await this.createCharacterHair() // await this.createCharacterEquipment() - // Zone - await this.createZone() + // Map + await this.createMap() // User await this.createUser() @@ -51,19 +51,19 @@ export default class InitCommand extends BaseCommand { } } - private async importObjects(): Promise { - for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) { + private async importMapObjects(): Promise { + for (const mapObject of fs.readdirSync(Storage.getPublicPath('map_objects'))) { const newMapObject = new MapObject() newMapObject - .setId(object.split('.')[0] as UUID) - .setName('New object') + .setId(mapObject.split('.')[0] as UUID) + .setName('New map object') .setFrameWidth( - (await sharp(Storage.getPublicPath('objects', object)) + (await sharp(Storage.getPublicPath('map_objects', mapObject)) .metadata() .then((metadata) => metadata.height)) ?? 0 ) .setFrameHeight( - (await sharp(Storage.getPublicPath('objects', object)) + (await sharp(Storage.getPublicPath('map_objects', mapObject)) .metadata() .then((metadata) => metadata.width)) ?? 0 ) @@ -224,17 +224,17 @@ export default class InitCommand extends BaseCommand { await equipmentSprite.save() } - private async createZone(): Promise { - const zone = new Zone() - await zone - .setName('New zone') + private async createMap(): Promise { + const map = new Map() + await map + .setName('New map') .setWidth(100) .setHeight(100) .setTiles(Array.from({ length: 100 }, () => Array.from({ length: 100 }, () => 'a2fd8d6f-5042-437a-9c1e-c66b91ecc35b'))) .save() - const effect = new ZoneEffect() - await effect.setEffect('light').setStrength(100).setZone(zone).save() + const effect = new MapEffect() + await effect.setEffect('light').setStrength(100).setMap(map).save() } private async createUser(): Promise { @@ -247,7 +247,7 @@ export default class InitCommand extends BaseCommand { .setUser(user) .setName('root') .setRole('gm') - .setZone((await ZoneRepository.getFirst())!) + .setMap((await MapRepository.getFirst())!) .setCharacterType((await CharacterTypeRepository.getFirst()) ?? undefined) .setCharacterHair((await CharacterHairRepository.getFirst()) ?? undefined) .save() diff --git a/src/commands/listZones.ts b/src/commands/listMaps.ts similarity index 53% rename from src/commands/listZones.ts rename to src/commands/listMaps.ts index 8a56ab7..0f8e474 100644 --- a/src/commands/listZones.ts +++ b/src/commands/listMaps.ts @@ -1,12 +1,12 @@ import { Server } from 'socket.io' import { BaseCommand } from '#application/base/baseCommand' -import ZoneManager from '#managers/zoneManager' +import MapManager from '#managers/mapManager' type CommandInput = string[] -export default class ListZonesCommand extends BaseCommand { +export default class ListMapsCommand extends BaseCommand { public execute(input: CommandInput): void { - console.log(ZoneManager.getLoadedZones()) + console.log(MapManager.getLoadedMaps()) } } diff --git a/src/entities/character.ts b/src/entities/character.ts index 3cd393c..46e2f4f 100644 --- a/src/entities/character.ts +++ b/src/entities/character.ts @@ -8,7 +8,7 @@ import { CharacterItem } from './characterItem' import { CharacterType } from './characterType' import { Chat } from './chat' import { User } from './user' -import { Zone } from './zone' +import { Map } from './map' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' @@ -35,7 +35,7 @@ export class Character extends BaseEntity { // Position @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() positionX = 0 @@ -142,13 +142,13 @@ export class Character extends BaseEntity { return this.chats } - setZone(zone: Zone) { - this.zone = zone + setMap(map: Map) { + this.map = map return this } - getZone() { - return this.zone + getMap() { + return this.map } setPositionX(positionX: number) { diff --git a/src/entities/characterType.ts b/src/entities/characterType.ts index 160d02a..b260730 100644 --- a/src/entities/characterType.ts +++ b/src/entities/characterType.ts @@ -26,10 +26,7 @@ export class CharacterType extends BaseEntity { @Property() isSelectable = false - @OneToMany(() => Character, (character) => character.characterType) - characters = new Collection(this) - - @ManyToOne(() => Sprite, { nullable: true }) + @ManyToOne({ nullable: true }) sprite?: Sprite @Property() @@ -109,13 +106,4 @@ export class CharacterType extends BaseEntity { getUpdatedAt() { return this.updatedAt } - - setCharacters(characters: Collection) { - this.characters = characters - return this - } - - getCharacters() { - return this.characters - } } diff --git a/src/entities/chat.ts b/src/entities/chat.ts index 680d541..7417640 100644 --- a/src/entities/chat.ts +++ b/src/entities/chat.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Character } from './character' -import { Zone } from './zone' +import { Map } from './map' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' @@ -17,7 +17,7 @@ export class Chat extends BaseEntity { character!: Character @ManyToOne({ deleteRule: 'cascade' }) - zone!: Zone + map!: Map @Property() message!: string @@ -43,13 +43,13 @@ export class Chat extends BaseEntity { return this.character } - setZone(zone: Zone) { - this.zone = zone + setMap(map: Map) { + this.map = map return this } - getZone() { - return this.zone + getMap() { + return this.map } setMessage(message: string) { diff --git a/src/entities/zone.ts b/src/entities/map.ts similarity index 57% rename from src/entities/zone.ts rename to src/entities/map.ts index 91c9f26..657711d 100644 --- a/src/entities/zone.ts +++ b/src/entities/map.ts @@ -4,16 +4,16 @@ import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/ import { Character } from './character' import { Chat } from './chat' -import { ZoneEffect } from './zoneEffect' -import { ZoneEventTile } from './zoneEventTile' -import { ZoneEventTileTeleport } from './zoneEventTileTeleport' -import { ZoneObject } from './zoneObject' +import { MapEffect } from './mapEffect' +import { MapEventTile } from './mapEventTile' +import { MapEventTileTeleport } from './mapEventTileTeleport' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' +import { PlacedMapObject } from '#entities/placedMapObject' @Entity() -export class Zone extends BaseEntity { +export class Map extends BaseEntity { @PrimaryKey() id = randomUUID() @@ -38,22 +38,22 @@ export class Zone extends BaseEntity { @Property() updatedAt = new Date() - @OneToMany(() => ZoneEffect, (effect) => effect.zone) - zoneEffects = new Collection(this) + @OneToMany(() => MapEffect, (effect) => effect.map) + mapEffects = new Collection(this) - @OneToMany(() => ZoneEventTile, (tile) => tile.zone) - zoneEventTiles = new Collection(this) + @OneToMany(() => MapEventTile, (tile) => tile.map) + mapEventTiles = new Collection(this) - @OneToMany(() => ZoneEventTileTeleport, (teleport) => teleport.toZone) - zoneEventTileTeleports = new Collection(this) + @OneToMany(() => MapEventTileTeleport, (teleport) => teleport.toMap) + mapEventTileTeleports = new Collection(this) - @OneToMany(() => ZoneObject, (object) => object.zone) - zoneObjects = new Collection(this) + @OneToMany(() => PlacedMapObject, (object) => object.map) + placedMapObjects = new Collection(this) - @OneToMany(() => Character, (character) => character.zone) + @OneToMany(() => Character, (character) => character.map) characters = new Collection(this) - @OneToMany(() => Chat, (chat) => chat.zone) + @OneToMany(() => Chat, (chat) => chat.map) chats = new Collection(this) setId(id: UUID) { @@ -128,40 +128,40 @@ export class Zone extends BaseEntity { return this.updatedAt } - setZoneEffects(zoneEffects: Collection) { - this.zoneEffects = zoneEffects + setMapEffects(mapEffects: Collection) { + this.mapEffects = mapEffects return this } - getZoneEffects() { - return this.zoneEffects + getMapEffects() { + return this.mapEffects } - setZoneEventTiles(zoneEventTiles: Collection) { - this.zoneEventTiles = zoneEventTiles + setMapEventTiles(mapEventTiles: Collection) { + this.mapEventTiles = mapEventTiles return this } - getZoneEventTiles() { - return this.zoneEventTiles + getMapEventTiles() { + return this.mapEventTiles } - setZoneEventTileTeleports(zoneEventTileTeleports: Collection) { - this.zoneEventTileTeleports = zoneEventTileTeleports + setMapEventTileTeleports(mapEventTileTeleports: Collection) { + this.mapEventTileTeleports = mapEventTileTeleports return this } - getZoneEventTileTeleports() { - return this.zoneEventTileTeleports + getMapEventTileTeleports() { + return this.mapEventTileTeleports } - setZoneObjects(zoneObjects: Collection) { - this.zoneObjects = zoneObjects + setPlacedMapObjects(placedMapObjects: Collection) { + this.placedMapObjects = placedMapObjects return this } - getZoneObjects() { - return this.zoneObjects + getPlacedMapObjects() { + return this.placedMapObjects } setCharacters(characters: Collection) { diff --git a/src/entities/zoneEffect.ts b/src/entities/mapEffect.ts similarity index 81% rename from src/entities/zoneEffect.ts rename to src/entities/mapEffect.ts index ff3ff64..73ad517 100644 --- a/src/entities/zoneEffect.ts +++ b/src/entities/mapEffect.ts @@ -2,18 +2,18 @@ import { randomUUID } from 'node:crypto' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' -import { Zone } from './zone' +import { Map } from './map' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' @Entity() -export class ZoneEffect extends BaseEntity { +export class MapEffect extends BaseEntity { @PrimaryKey() id = randomUUID() @ManyToOne({ deleteRule: 'cascade' }) - zone!: Zone + map!: Map @Property() effect!: string @@ -30,13 +30,13 @@ export class ZoneEffect extends BaseEntity { return this.id } - setZone(zone: Zone) { - this.zone = zone + setMap(map: Map) { + this.map = map return this } - getZone() { - return this.zone + getMap() { + return this.map } setEffect(effect: string) { diff --git a/src/entities/zoneEventTile.ts b/src/entities/mapEventTile.ts similarity index 63% rename from src/entities/zoneEventTile.ts rename to src/entities/mapEventTile.ts index 13951c5..3e33b2e 100644 --- a/src/entities/zoneEventTile.ts +++ b/src/entities/mapEventTile.ts @@ -2,23 +2,23 @@ import { randomUUID } from 'node:crypto' import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' -import { Zone } from './zone' -import { ZoneEventTileTeleport } from './zoneEventTileTeleport' +import { Map } from './map' +import { MapEventTileTeleport } from './mapEventTileTeleport' import { BaseEntity } from '#application/base/baseEntity' -import { ZoneEventTileType } from '#application/enums' +import { MapEventTileType } from '#application/enums' import { UUID } from '#application/types' @Entity() -export class ZoneEventTile extends BaseEntity { +export class MapEventTile extends BaseEntity { @PrimaryKey() id = randomUUID() @ManyToOne({ deleteRule: 'cascade' }) - zone!: Zone + map!: Map - @Enum(() => ZoneEventTileType) - type!: ZoneEventTileType + @Enum(() => MapEventTileType) + type!: MapEventTileType @Property() positionX!: number @@ -26,8 +26,8 @@ export class ZoneEventTile extends BaseEntity { @Property() positionY!: number - @OneToOne(() => ZoneEventTileTeleport, (teleport) => teleport.zoneEventTile) - teleport?: ZoneEventTileTeleport + @OneToOne(() => MapEventTileTeleport, (teleport) => teleport.mapEventTile) + teleport?: MapEventTileTeleport setId(id: UUID) { this.id = id @@ -38,16 +38,16 @@ export class ZoneEventTile extends BaseEntity { return this.id } - setZone(zone: Zone) { - this.zone = zone + setMap(map: Map) { + this.map = map return this } - getZone() { - return this.zone + getMap() { + return this.map } - setType(type: ZoneEventTileType) { + setType(type: MapEventTileType) { this.type = type return this } @@ -74,7 +74,7 @@ export class ZoneEventTile extends BaseEntity { return this.positionY } - setTeleport(teleport: ZoneEventTileTeleport) { + setTeleport(teleport: MapEventTileTeleport) { this.teleport = teleport return this } diff --git a/src/entities/zoneEventTileTeleport.ts b/src/entities/mapEventTileTeleport.ts similarity index 71% rename from src/entities/zoneEventTileTeleport.ts rename to src/entities/mapEventTileTeleport.ts index d87f375..1d62b97 100644 --- a/src/entities/zoneEventTileTeleport.ts +++ b/src/entities/mapEventTileTeleport.ts @@ -2,22 +2,22 @@ import { randomUUID } from 'node:crypto' import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' -import { Zone } from './zone' -import { ZoneEventTile } from './zoneEventTile' +import { Map } from './map' +import { MapEventTile } from './mapEventTile' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' @Entity() -export class ZoneEventTileTeleport extends BaseEntity { +export class MapEventTileTeleport extends BaseEntity { @PrimaryKey() id = randomUUID() @OneToOne({ deleteRule: 'cascade' }) - zoneEventTile!: ZoneEventTile + mapEventTile!: MapEventTile @ManyToOne({ deleteRule: 'cascade' }) - toZone!: Zone + toMap!: Map @Property() toRotation!: number @@ -37,22 +37,22 @@ export class ZoneEventTileTeleport extends BaseEntity { return this.id } - setZoneEventTile(zoneEventTile: ZoneEventTile) { - this.zoneEventTile = zoneEventTile + setMapEventTile(mapEventTile: MapEventTile) { + this.mapEventTile = mapEventTile return this } - getZoneEventTile() { - return this.zoneEventTile + getMapEventTile() { + return this.mapEventTile } - setToZone(toZone: Zone) { - this.toZone = toZone + setToMap(toMap: Map) { + this.toMap = toMap return this } - getToZone() { - return this.toZone + getToMap() { + return this.toMap } setToRotation(toRotation: number) { diff --git a/src/entities/mapObject.ts b/src/entities/mapObject.ts index 385bbd0..3debba2 100644 --- a/src/entities/mapObject.ts +++ b/src/entities/mapObject.ts @@ -1,8 +1,6 @@ import { randomUUID } from 'node:crypto' -import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' - -import { ZoneObject } from './zoneObject' +import { Entity, PrimaryKey, Property } from '@mikro-orm/core' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' @@ -18,10 +16,10 @@ export class MapObject extends BaseEntity { @Property({ type: 'json', nullable: true }) tags?: any - @Property() + @Property({ type: 'decimal', precision: 10, scale: 2 }) originX = 0 - @Property() + @Property({ type: 'decimal', precision: 10, scale: 2 }) originY = 0 @Property() diff --git a/src/entities/zoneObject.ts b/src/entities/placedMapObject.ts similarity index 88% rename from src/entities/zoneObject.ts rename to src/entities/placedMapObject.ts index f71854a..72e7ed8 100644 --- a/src/entities/zoneObject.ts +++ b/src/entities/placedMapObject.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' -import { Zone } from './zone' +import { Map } from './map' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' @@ -10,12 +10,12 @@ import { MapObject } from '#entities/mapObject' //@TODO : Rename mapObject @Entity() -export class ZoneObject extends BaseEntity { +export class PlacedMapObject extends BaseEntity { @PrimaryKey() id = randomUUID() @ManyToOne({ deleteRule: 'cascade' }) - zone!: Zone + map!: Map @ManyToOne({ deleteRule: 'cascade' }) mapObject!: MapObject @@ -41,13 +41,13 @@ export class ZoneObject extends BaseEntity { return this.id } - setZone(zone: Zone) { - this.zone = zone + setMap(map: Map) { + this.map = map return this } - getZone() { - return this.zone + getMap() { + return this.map } setMapObject(mapObject: MapObject) { diff --git a/src/events/character/charactersScreen/characterHairList.ts b/src/events/character/charactersScreen/characterHairList.ts index 47e7e49..c1dacc5 100644 --- a/src/events/character/charactersScreen/characterHairList.ts +++ b/src/events/character/charactersScreen/characterHairList.ts @@ -11,8 +11,13 @@ export default class characterHairListEvent extends BaseEvent { } private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise { - const items: CharacterHair[] = await characterHairRepository.getAllSelectable() - await Database.getEntityManager().populate(items, ['sprite']) - callback(items) + try { + const items: CharacterHair[] = await characterHairRepository.getAllSelectable() + await Database.getEntityManager().populate(items, ['sprite']) + return callback(items) + } catch (error) { + this.logger.error('character:hair:list error', error) + return callback([]) + } } } diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index accea3c..bff78d1 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -1,6 +1,6 @@ import { BaseEvent } from '#application/base/baseEvent' import { UUID } from '#application/types' -import ZoneManager from '#managers/zoneManager' +import MapManager from '#managers/mapManager' import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterRepository from '#repositories/characterRepository' import TeleportService from '#services/teleportService' @@ -15,30 +15,14 @@ export default class CharacterConnectEvent extends BaseEvent { 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 { - if (!this.socket.userId) { - this.emitError('User not authenticated') - return - } - try { if (await this.checkForActiveCharacters()) { this.emitError('You are already connected to another character') return } - const character = await CharacterRepository.getByUserAndId(this.socket.userId, data.characterId) + const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId) if (!character) { 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 callback({ character }) - // wait 300 ms, @TODO: Find a better way to do this - await new Promise((resolve) => setTimeout(resolve, 100)) + // wait 300 ms, @TODO: Find a better way to do this, race condition + await new Promise((resolve) => setTimeout(resolve, 500)) await TeleportService.teleportCharacter(character.id, { - targetZoneId: character.zone.id, + targetMapId: character.map.id, targetX: character.positionX, targetY: character.positionY, rotation: character.rotation, @@ -75,6 +59,6 @@ export default class CharacterConnectEvent extends BaseEvent { private async checkForActiveCharacters(): Promise { 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 } } diff --git a/src/events/character/create.ts b/src/events/character/create.ts index e613477..f343559 100644 --- a/src/events/character/create.ts +++ b/src/events/character/create.ts @@ -5,7 +5,7 @@ import { ZCharacterCreate } from '#application/zodTypes' import { Character } from '#entities/character' import CharacterRepository from '#repositories/characterRepository' import UserRepository from '#repositories/userRepository' -import ZoneRepository from '#repositories/zoneRepository' +import MapRepository from '#repositories/mapRepository' export default class CharacterCreateEvent extends BaseEvent { public listen(): void { @@ -37,10 +37,10 @@ export default class CharacterCreateEvent extends BaseEvent { } // @TODO: Change to default location - const zone = await ZoneRepository.getFirst() + const map = await MapRepository.getFirst() 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) { return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' }) diff --git a/src/events/character/delete.ts b/src/events/character/delete.ts index 2a558cd..a077b5a 100644 --- a/src/events/character/delete.ts +++ b/src/events/character/delete.ts @@ -1,7 +1,7 @@ import { BaseEvent } from '#application/base/baseEvent' import { UUID } from '#application/types' import { Character } from '#entities/character' -import { Zone } from '#entities/zone' +import { Map } from '#entities/map' import CharacterRepository from '#repositories/characterRepository' type TypePayload = { @@ -9,7 +9,6 @@ type TypePayload = { } type TypeResponse = { - zone: Zone characters: Character[] } @@ -20,11 +19,7 @@ export default class CharacterDeleteEvent extends BaseEvent { private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise { try { - const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId) - if (character) { - await character.delete() - } - + await (await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId))?.delete() const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) this.socket.emit('character:list', characters) diff --git a/src/events/character/list.ts b/src/events/character/list.ts index d7836be..01e4a6c 100644 --- a/src/events/character/list.ts +++ b/src/events/character/list.ts @@ -1,5 +1,4 @@ import { BaseEvent } from '#application/base/baseEvent' -import Database from '#application/database' import { Character } from '#entities/character' import CharacterRepository from '#repositories/characterRepository' @@ -10,9 +9,7 @@ export default class CharacterListEvent extends BaseEvent { private async handleEvent(data: any): Promise { try { - const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) - await Database.getEntityManager().populate(characters, ['characterType', 'characterHair']) - + let characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!, ['characterType', 'characterHair']) this.socket.emit('character:list', characters) } catch (error: any) { this.logger.error('character:list error', error.message) diff --git a/src/events/chat/gameMaster/teleportCommand.ts b/src/events/chat/gameMaster/teleportCommand.ts index e969d54..a0a8714 100644 --- a/src/events/chat/gameMaster/teleportCommand.ts +++ b/src/events/chat/gameMaster/teleportCommand.ts @@ -1,7 +1,7 @@ import { BaseEvent } from '#application/base/baseEvent' import { UUID } from '#application/types' -import ZoneManager from '#managers/zoneManager' -import ZoneRepository from '#repositories/zoneRepository' +import MapManager from '#managers/mapManager' +import MapRepository from '#repositories/mapRepository' import ChatService from '#services/chatService' import TeleportService from '#services/teleportService' @@ -16,13 +16,13 @@ export default class TeleportCommandEvent extends BaseEvent { private async handleEvent(data: TypePayload, callback: (response: boolean) => void) { try { - const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) - if (!zoneCharacter) { + const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) + if (!mapCharacter) { this.logger.error('chat:message error', 'Character not found') return } - const character = zoneCharacter.character + const character = mapCharacter.character if (character.role !== 'gm') { 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) { this.socket.emit('notification', { title: 'Server message', - message: 'Usage: /teleport [x] [y]' + message: 'Usage: /teleport [x] [y]' }) return } - const zoneId = args[0] as UUID + const mapId = args[0] as UUID const targetX = args[1] ? parseInt(args[1], 10) : 0 const targetY = args[2] ? parseInt(args[2], 10) : 0 - if (!zoneId || isNaN(targetX) || isNaN(targetY)) { + if (!mapId || isNaN(targetX) || isNaN(targetY)) { this.socket.emit('notification', { title: 'Server message', message: 'Invalid parameters. X and Y coordinates must be numbers.' @@ -53,16 +53,16 @@ export default class TeleportCommandEvent extends BaseEvent { return } - const zone = await ZoneRepository.getById(zoneId) - if (!zone) { + const map = await MapRepository.getById(mapId) + if (!map) { this.socket.emit('notification', { title: 'Server message', - message: 'Zone not found' + message: 'Map not found' }) 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', { title: 'Server message', message: 'You are already at that location' @@ -71,7 +71,7 @@ export default class TeleportCommandEvent extends BaseEvent { } const success = await TeleportService.teleportCharacter(character.id, { - targetZoneId: zone.id, + targetMapId: map.id, targetX, targetY, rotation: character.rotation @@ -86,9 +86,9 @@ export default class TeleportCommandEvent extends BaseEvent { this.socket.emit('notification', { 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) { this.logger.error(`Error in teleport command: ${error.message}`) this.socket.emit('notification', { diff --git a/src/events/chat/message.ts b/src/events/chat/message.ts index 13a27f9..7946032 100644 --- a/src/events/chat/message.ts +++ b/src/events/chat/message.ts @@ -1,6 +1,6 @@ import { BaseEvent } from '#application/base/baseEvent' -import ZoneManager from '#managers/zoneManager' -import ZoneRepository from '#repositories/zoneRepository' +import MapManager from '#managers/mapManager' +import MapRepository from '#repositories/mapRepository' import ChatService from '#services/chatService' type TypePayload = { @@ -18,21 +18,21 @@ export default class ChatMessageEvent extends BaseEvent { return callback(false) } - const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) - if (!zoneCharacter) { + const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) + if (!mapCharacter) { this.logger.error('chat:message error', 'Character not found') return callback(false) } - const character = zoneCharacter.character + const character = mapCharacter.character - const zone = await ZoneRepository.getById(character.zone.id) - if (!zone) { - this.logger.error('chat:message error', 'Zone not found') + const map = await MapRepository.getById(character.map.id) + if (!map) { + this.logger.error('chat:message error', 'Map not found') return callback(false) } - if (await ChatService.sendZoneMessage(character.getId(), zone.getId(), data.message)) { + if (await ChatService.sendMapMessage(character.getId(), map.getId(), data.message)) { return callback(true) } diff --git a/src/events/disconnect.ts b/src/events/disconnect.ts index 8bb7e9c..fc2c912 100644 --- a/src/events/disconnect.ts +++ b/src/events/disconnect.ts @@ -1,5 +1,5 @@ import { BaseEvent } from '#application/base/baseEvent' -import ZoneManager from '#managers/zoneManager' +import MapManager from '#managers/mapManager' export default class DisconnectEvent extends BaseEvent { public listen(): void { @@ -15,13 +15,13 @@ export default class DisconnectEvent extends BaseEvent { this.io.emit('user:disconnect', this.socket.userId) - const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!) - if (!zoneCharacter) { + const mapCharacter = MapManager.getCharacterById(this.socket.characterId!) + if (!mapCharacter) { this.logger.info('User disconnected but had no character set') return } - await zoneCharacter.disconnect(this.socket, this.io) + await mapCharacter.disconnect(this.socket, this.io) this.logger.info('User disconnected along with their character') } catch (error: any) { this.logger.error('disconnect error: ' + error.message) diff --git a/src/events/gameMaster/assetManager/characterHair/delete.ts b/src/events/gameMaster/assetManager/characterHair/delete.ts index 0f80778..f4b91c8 100644 --- a/src/events/gameMaster/assetManager/characterHair/delete.ts +++ b/src/events/gameMaster/assetManager/characterHair/delete.ts @@ -1,9 +1,9 @@ import { BaseEvent } from '#application/base/baseEvent' import CharacterHairRepository from '#repositories/characterHairRepository' -import characterRepository from '#repositories/characterRepository' +import { UUID } from '#application/types' interface IPayload { - id: number + id: UUID } 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 { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) return callback(false) - - if (character.role !== 'gm') { - return callback(false) - } - try { - const characterHair = await CharacterHairRepository.getById(data.id) - if (characterHair) { - await characterHair.delete() - } + if (!(await this.isCharacterGM())) return - callback(true) + const characterHair = await CharacterHairRepository.getById(data.id) + await (await CharacterHairRepository.getById(data.id))?.delete() + + return callback(true) } catch (error) { this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) callback(false) diff --git a/src/events/gameMaster/assetManager/characterHair/list.ts b/src/events/gameMaster/assetManager/characterHair/list.ts index d6ea78d..9b111f8 100644 --- a/src/events/gameMaster/assetManager/characterHair/list.ts +++ b/src/events/gameMaster/assetManager/characterHair/list.ts @@ -1,7 +1,6 @@ import { BaseEvent } from '#application/base/baseEvent' import { CharacterHair } from '#entities/characterHair' import characterHairRepository from '#repositories/characterHairRepository' -import characterRepository from '#repositories/characterRepository' interface IPayload {} @@ -11,19 +10,14 @@ export default class characterHairListEvent extends BaseEvent { } private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) { - this.logger.error('gm:characterHair:list error', 'Character not found') + try { + if (!(await this.isCharacterGM())) return + + const items = await characterHairRepository.getAll() + return callback(items) + } catch (error) { + this.logger.error('gm:characterHair:list error', error) return callback([]) } - - if (character.role !== 'gm') { - this.logger.info(`User ${character.id} tried to list character hair but is not a game master.`) - return callback([]) - } - - // get all objects - const items = await characterHairRepository.getAll() - callback(items) } } diff --git a/src/events/gameMaster/assetManager/characterHair/update.ts b/src/events/gameMaster/assetManager/characterHair/update.ts index 87b1667..a882ce1 100644 --- a/src/events/gameMaster/assetManager/characterHair/update.ts +++ b/src/events/gameMaster/assetManager/characterHair/update.ts @@ -6,7 +6,7 @@ import characterRepository from '#repositories/characterRepository' import SpriteRepository from '#repositories/spriteRepository' type Payload = { - id: number + id: UUID name: string gender: CharacterGender isSelectable: boolean @@ -19,21 +19,17 @@ export default class CharacterHairUpdateEvent extends BaseEvent { } private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) return callback(false) - - if (character.role !== 'gm') { - return callback(false) - } - try { + if (!(await this.isCharacterGM())) return + const sprite = await SpriteRepository.getById(data.spriteId) const characterHair = await CharacterHairRepository.getById(data.id) - if (characterHair) { - await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update() + if (!characterHair) { + return callback(false) } + await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update() return callback(true) } catch (error) { this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`) diff --git a/src/events/gameMaster/assetManager/characterType/create.ts b/src/events/gameMaster/assetManager/characterType/create.ts index 4bbe104..f47170f 100644 --- a/src/events/gameMaster/assetManager/characterType/create.ts +++ b/src/events/gameMaster/assetManager/characterType/create.ts @@ -1,41 +1,22 @@ -import { CharacterGender, CharacterRace } from '@prisma/client' -import { Server } from 'socket.io' - -import prisma from '#application/prisma' -import { TSocket } from '#application/types' -import characterRepository from '#repositories/characterRepository' - -export default class CharacterTypeCreateEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} +import { BaseEvent } from '#application/base/baseEvent' +import { CharacterType } from '#entities/characterType' +export default class CharacterTypeCreateEvent extends BaseEvent { public listen(): void { this.socket.on('gm:characterType:create', this.handleEvent.bind(this)) } private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise { try { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) return callback(false) + if (!(await this.isCharacterGM())) return - if (character.role !== 'gm') { - return callback(false) - } + const newCharacterType = new CharacterType() + await newCharacterType.setName('New character type').save() - const newCharacterType = await prisma.characterType.create({ - data: { - name: 'New character type', - gender: CharacterGender.MALE, - race: CharacterRace.HUMAN - } - }) - - callback(true, newCharacterType) + return callback(true, newCharacterType) } catch (error) { console.error('Error creating character type:', error) - callback(false) + return callback(false) } } } diff --git a/src/events/gameMaster/assetManager/characterType/delete.ts b/src/events/gameMaster/assetManager/characterType/delete.ts index 321077c..3d8039b 100644 --- a/src/events/gameMaster/assetManager/characterType/delete.ts +++ b/src/events/gameMaster/assetManager/characterType/delete.ts @@ -1,41 +1,28 @@ -import { Server } from 'socket.io' - -import { gameMasterLogger } from '#application/logger' -import { TSocket } from '#application/types' -import characterRepository from '#repositories/characterRepository' +import { UUID } from '#application/types' import CharacterTypeRepository from '#repositories/characterTypeRepository' +import { BaseEvent } from '#application/base/baseEvent' interface IPayload { - id: number + id: UUID } -export default class CharacterTypeDeleteEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} - +export default class CharacterTypeDeleteEvent extends BaseEvent { public listen(): void { this.socket.on('gm:characterType:remove', this.handleEvent.bind(this)) } private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId!) - if (!character) return callback(false) - - if (character.role !== 'gm') { - return callback(false) - } - try { + if (!(await this.isCharacterGM())) return + const characterType = await CharacterTypeRepository.getById(data.id) if (!characterType) return callback(false) await characterType.delete() - callback(true) + return callback(true) } catch (error) { - gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) - callback(false) + this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) + return callback(false) } } } diff --git a/src/events/gameMaster/assetManager/characterType/list.ts b/src/events/gameMaster/assetManager/characterType/list.ts index 15ff718..7436888 100644 --- a/src/events/gameMaster/assetManager/characterType/list.ts +++ b/src/events/gameMaster/assetManager/characterType/list.ts @@ -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 { BaseEvent } from '#application/base/baseEvent' +import { CharacterType } from '#entities/characterType' interface IPayload {} -export default class CharacterTypeListEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} - +export default class CharacterTypeListEvent extends BaseEvent { public listen(): void { this.socket.on('gm:characterType:list', this.handleEvent.bind(this)) } private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) { - gameMasterLogger.error('gm:characterType:list error', 'Character not found') + try { + if (!(await this.isCharacterGM())) return + + const items = await CharacterTypeRepository.getAll() + return callback(items) + } catch (error) { + this.logger.error('gm:characterType:list error', error) return callback([]) } - - if (character.role !== 'gm') { - gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`) - return callback([]) - } - - // get all objects - const items = await CharacterTypeRepository.getAll() - callback(items) } } diff --git a/src/events/gameMaster/assetManager/mapObject/list.ts b/src/events/gameMaster/assetManager/mapObject/list.ts new file mode 100644 index 0000000..477e336 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/list.ts @@ -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 { + if (!(await this.isCharacterGM())) return + + // get all objects + const objects = await ObjectRepository.getAll() + return callback(objects) + } +} diff --git a/src/events/gameMaster/assetManager/mapObject/remove.ts b/src/events/gameMaster/assetManager/mapObject/remove.ts new file mode 100644 index 0000000..2eda1b3 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/remove.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/assetManager/mapObject/update.ts b/src/events/gameMaster/assetManager/mapObject/update.ts new file mode 100644 index 0000000..2e288e0 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/update.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/assetManager/mapObject/upload.ts b/src/events/gameMaster/assetManager/mapObject/upload.ts new file mode 100644 index 0000000..6015659 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/upload.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/assetManager/object/list.ts b/src/events/gameMaster/assetManager/object/list.ts deleted file mode 100644 index 60b3dc6..0000000 --- a/src/events/gameMaster/assetManager/object/list.ts +++ /dev/null @@ -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 { - 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) - } -} diff --git a/src/events/gameMaster/assetManager/object/remove.ts b/src/events/gameMaster/assetManager/object/remove.ts deleted file mode 100644 index 6f63eef..0000000 --- a/src/events/gameMaster/assetManager/object/remove.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/assetManager/object/update.ts b/src/events/gameMaster/assetManager/object/update.ts deleted file mode 100644 index 305a927..0000000 --- a/src/events/gameMaster/assetManager/object/update.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/assetManager/object/upload.ts b/src/events/gameMaster/assetManager/object/upload.ts deleted file mode 100644 index 840ee66..0000000 --- a/src/events/gameMaster/assetManager/object/upload.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/assetManager/sprite/create.ts b/src/events/gameMaster/assetManager/sprite/create.ts index 25103b0..4ca8877 100644 --- a/src/events/gameMaster/assetManager/sprite/create.ts +++ b/src/events/gameMaster/assetManager/sprite/create.ts @@ -1,16 +1,10 @@ import fs from 'fs/promises' -import { Server } from 'socket.io' - import { BaseEvent } from '#application/base/baseEvent' import Storage from '#application/storage' +import { Sprite } from '#entities/sprite' export default class SpriteCreateEvent extends BaseEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} - public listen(): void { this.socket.on('gm:sprite:create', this.handleEvent.bind(this)) } @@ -24,21 +18,19 @@ export default class SpriteCreateEvent extends BaseEvent { // Ensure the folder exists await fs.mkdir(public_folder, { recursive: true }) - const sprite = await prisma.sprite.create({ - data: { - name: 'New sprite' - } - }) + const sprite = new Sprite() + await sprite.setName('New sprite').save() + const uuid = sprite.id // Create folder with uuid const sprite_folder = Storage.getPublicPath('sprites', uuid) await fs.mkdir(sprite_folder, { recursive: true }) - callback(true) + return callback(true) } catch (error) { console.error('Error creating sprite:', error) - callback(false) + return callback(false) } } } diff --git a/src/events/gameMaster/zoneEditor/create.ts b/src/events/gameMaster/mapEditor/create.ts similarity index 56% rename from src/events/gameMaster/zoneEditor/create.ts rename to src/events/gameMaster/mapEditor/create.ts index 91857de..60c168f 100644 --- a/src/events/gameMaster/zoneEditor/create.ts +++ b/src/events/gameMaster/mapEditor/create.ts @@ -1,6 +1,6 @@ import { BaseEvent } from '#application/base/baseEvent' -import { Zone } from '#entities/zone' -import ZoneRepository from '#repositories/zoneRepository' +import { Map } from '#entities/map' +import MapRepository from '#repositories/mapRepository' type Payload = { name: string @@ -8,30 +8,30 @@ type Payload = { height: number } -export default class ZoneCreateEvent extends BaseEvent { +export default class MapCreateEvent extends BaseEvent { 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 { + private async handleEvent(data: Payload, callback: (response: Map[]) => void): Promise { try { 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() - await zone + const map = new Map() + await map .setName(data.name) .setWidth(data.width) .setHeight(data.height) .setTiles(Array.from({ length: data.height }, () => Array.from({ length: data.width }, () => 'blank_tile'))) .save() - const zoneList = await ZoneRepository.getAll() - return callback(zoneList) + const mapList = await MapRepository.getAll() + return callback(mapList) } catch (error: any) { - this.logger.error('gm:zone_editor:zone:create error', error.message) - this.socket.emit('notification', { message: 'Failed to create zone.' }) + this.logger.error('gm:map:create error', error.message) + this.socket.emit('notification', { message: 'Failed to create map.' }) return callback([]) } } diff --git a/src/events/gameMaster/mapEditor/delete.ts b/src/events/gameMaster/mapEditor/delete.ts new file mode 100644 index 0000000..80d92ea --- /dev/null +++ b/src/events/gameMaster/mapEditor/delete.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/mapEditor/list.ts b/src/events/gameMaster/mapEditor/list.ts new file mode 100644 index 0000000..4161a1b --- /dev/null +++ b/src/events/gameMaster/mapEditor/list.ts @@ -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 { + 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([]) + } + } +} diff --git a/src/events/gameMaster/mapEditor/request.ts b/src/events/gameMaster/mapEditor/request.ts new file mode 100644 index 0000000..f5e95c7 --- /dev/null +++ b/src/events/gameMaster/mapEditor/request.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/mapEditor/update.ts b/src/events/gameMaster/mapEditor/update.ts new file mode 100644 index 0000000..4b11a62 --- /dev/null +++ b/src/events/gameMaster/mapEditor/update.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/zoneEditor/delete.ts b/src/events/gameMaster/zoneEditor/delete.ts deleted file mode 100644 index 44ac4e6..0000000 --- a/src/events/gameMaster/zoneEditor/delete.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/zoneEditor/list.ts b/src/events/gameMaster/zoneEditor/list.ts deleted file mode 100644 index 86c41c6..0000000 --- a/src/events/gameMaster/zoneEditor/list.ts +++ /dev/null @@ -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 { - 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([]) - } - } -} diff --git a/src/events/gameMaster/zoneEditor/request.ts b/src/events/gameMaster/zoneEditor/request.ts deleted file mode 100644 index 81895dd..0000000 --- a/src/events/gameMaster/zoneEditor/request.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/zoneEditor/update.ts b/src/events/gameMaster/zoneEditor/update.ts deleted file mode 100644 index 00dff01..0000000 --- a/src/events/gameMaster/zoneEditor/update.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/map/characterMove.ts b/src/events/map/characterMove.ts new file mode 100644 index 0000000..5402bc4 --- /dev/null +++ b/src/events/map/characterMove.ts @@ -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 { + 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 { + 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 { + 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 + }) + } +} diff --git a/src/events/zone/weather.ts b/src/events/map/weather.ts similarity index 100% rename from src/events/zone/weather.ts rename to src/events/map/weather.ts diff --git a/src/http/controllers/assets.ts b/src/http/controllers/assets.ts index 758cb69..8c1bfd9 100644 --- a/src/http/controllers/assets.ts +++ b/src/http/controllers/assets.ts @@ -8,7 +8,7 @@ import Storage from '#application/storage' import { AssetData, UUID } from '#application/types' import SpriteRepository from '#repositories/spriteRepository' import TileRepository from '#repositories/tileRepository' -import ZoneRepository from '#repositories/zoneRepository' +import MapRepository from '#repositories/mapRepository' 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 res */ - public async listTilesByZone(req: Request, res: Response) { - const zoneId = req.params.zoneId as UUID + public async listTilesByMap(req: Request, res: Response) { + const mapId = req.params.mapId as UUID - if (!zoneId) { - return this.sendError(res, 'Invalid zone ID', 400) + if (!mapId) { + return this.sendError(res, 'Invalid map ID', 400) } - const zone = await ZoneRepository.getById(zoneId) - if (!zone) { - return this.sendError(res, 'Zone not found', 404) + const map = await MapRepository.getById(mapId) + if (!map) { + return this.sendError(res, 'Map not found', 404) } const assets: AssetData[] = [] - const tiles = await TileRepository.getByZoneId(zoneId) + const tiles = await TileRepository.getByMapId(mapId) for (const tile of tiles) { 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']) 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', - group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites', + group: spriteAction.getIsAnimated() ? 'sprite_animations' : 'sprites', updatedAt: sprite.getUpdatedAt(), - originX: Number(spriteAction.originX.toString()), - originY: Number(spriteAction.originY.toString()), + originX: Number(spriteAction.getOriginX().toString()), + originY: Number(spriteAction.getOriginY().toString()), isAnimated: spriteAction.getIsAnimated(), frameRate: spriteAction.getFrameRate(), frameWidth: spriteAction.getFrameWidth(), frameHeight: spriteAction.getFrameHeight(), - frameCount: JSON.parse(JSON.stringify(spriteAction.getSprites())).length + frameCount: spriteAction.getSprites()?.length })) return this.sendSuccess(res, assets) diff --git a/src/managers/httpManager.ts b/src/managers/httpManager.ts index 957eab5..83e430a 100644 --- a/src/managers/httpManager.ts +++ b/src/managers/httpManager.ts @@ -33,7 +33,7 @@ class HttpManager { // Assets routes 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/:type/:spriteId?/:file', (req, res) => this.assetsController.downloadAsset(req, res)) } diff --git a/src/managers/mapManager.ts b/src/managers/mapManager.ts new file mode 100644 index 0000000..cdb4cae --- /dev/null +++ b/src/managers/mapManager.ts @@ -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 = {} + private logger = Logger.type(LoggerType.GAME) + + public async boot(): Promise { + 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 { + 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() \ No newline at end of file diff --git a/src/managers/zoneManager.ts b/src/managers/zoneManager.ts deleted file mode 100644 index 786fc0d..0000000 --- a/src/managers/zoneManager.ts +++ /dev/null @@ -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() - private logger = Logger.type(LoggerType.GAME) - - public async boot(): Promise { - 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 { - 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() diff --git a/src/models/loadedMap.ts b/src/models/loadedMap.ts new file mode 100644 index 0000000..ff65e67 --- /dev/null +++ b/src/models/loadedMap.ts @@ -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 { + 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 diff --git a/src/models/loadedZone.ts b/src/models/loadedZone.ts deleted file mode 100644 index 92d54a9..0000000 --- a/src/models/loadedZone.ts +++ /dev/null @@ -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 { - 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 diff --git a/src/models/zoneCharacter.ts b/src/models/mapCharacter.ts similarity index 67% rename from src/models/zoneCharacter.ts rename to src/models/mapCharacter.ts index 008d835..3fc2aa2 100644 --- a/src/models/zoneCharacter.ts +++ b/src/models/mapCharacter.ts @@ -3,10 +3,10 @@ import { Server } from 'socket.io' import { TSocket } from '#application/types' import { Character } from '#entities/character' import SocketManager from '#managers/socketManager' -import ZoneManager from '#managers/zoneManager' +import MapManager from '#managers/mapManager' import TeleportService from '#services/teleportService' -class ZoneCharacter { +class MapCharacter { public readonly character: Character public isMoving: boolean = false public currentPath: Array<{ x: number; y: number }> | null = null @@ -16,12 +16,12 @@ class ZoneCharacter { } 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 { + public async teleport(mapId: number, targetX: number, targetY: number): Promise { await TeleportService.teleportCharacter(this.character.id, { - targetZoneId: zoneId, + targetMapId: mapId, targetX, targetY }) @@ -34,13 +34,13 @@ class ZoneCharacter { this.currentPath = null await this.savePosition() - // Leave zone and remove from manager - if (this.character.zone) { - socket.leave(this.character.zone.id) - ZoneManager.removeCharacter(this.character.id) + // Leave map and remove from manager + if (this.character.map) { + socket.leave(this.character.map.id) + MapManager.removeCharacter(this.character.id) - // Notify zone players - io.in(this.character.zone.id).emit('zone:character:leave', this.character.id) + // Notify map players + io.in(this.character.map.id).emit('map:character:leave', this.character.id) } // Notify all players @@ -51,4 +51,4 @@ class ZoneCharacter { } } -export default ZoneCharacter +export default MapCharacter diff --git a/src/repositories/characterRepository.ts b/src/repositories/characterRepository.ts index 87e31c5..ddacc3f 100644 --- a/src/repositories/characterRepository.ts +++ b/src/repositories/characterRepository.ts @@ -1,12 +1,13 @@ import { BaseRepository } from '#application/base/baseRepository' import { UUID } from '#application/types' import { Character } from '#entities/character' +import { LoadHint, Populate } from '@mikro-orm/core' class CharacterRepository extends BaseRepository { - async getByUserId(userId: UUID): Promise { + async getByUserId(userId: UUID, populate?: LoadHint): Promise { try { const repository = this.em.getRepository(Character) - return await repository.find({ user: userId }) + return await repository.find({ user: userId }, { populate: populate as Populate }) } catch (error: any) { this.logger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`) return [] diff --git a/src/repositories/characterTypeRepository.ts b/src/repositories/characterTypeRepository.ts index dd763f3..78051f6 100644 --- a/src/repositories/characterTypeRepository.ts +++ b/src/repositories/characterTypeRepository.ts @@ -19,7 +19,7 @@ class CharacterTypeRepository extends BaseRepository { return await repository.findAll() } catch (error: any) { this.logger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`) - return null + return [] } } diff --git a/src/repositories/chatRepository.ts b/src/repositories/chatRepository.ts index a4495b3..e6807c2 100644 --- a/src/repositories/chatRepository.ts +++ b/src/repositories/chatRepository.ts @@ -35,12 +35,12 @@ class ChatRepository extends BaseRepository { } } - async getByZoneId(zoneId: UUID): Promise { + async getByMapId(mapId: UUID): Promise { try { const repository = this.em.getRepository(Chat) - return await repository.find({ zone: zoneId }) + return await repository.find({ map: mapId }) } 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 [] } } diff --git a/src/repositories/mapEventTileRepository.ts b/src/repositories/mapEventTileRepository.ts new file mode 100644 index 0000000..6e2473a --- /dev/null +++ b/src/repositories/mapEventTileRepository.ts @@ -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 { + 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() diff --git a/src/repositories/mapObjectRepository.ts b/src/repositories/mapObjectRepository.ts new file mode 100644 index 0000000..13290ae --- /dev/null +++ b/src/repositories/mapObjectRepository.ts @@ -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 { + try { + const repository = this.em.getRepository(MapObject) + return await repository.findOne({ id }) + } catch (error: any) { + return null + } + } + + async getAll(): Promise { + try { + const repository = this.em.getRepository(MapObject) + return await repository.findAll() + } catch (error: any) { + return [] + } + } +} + +export default new MapObjectRepository() diff --git a/src/repositories/mapRepository.ts b/src/repositories/mapRepository.ts new file mode 100644 index 0000000..7708ed3 --- /dev/null +++ b/src/repositories/mapRepository.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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() diff --git a/src/repositories/objectRepository.ts b/src/repositories/objectRepository.ts deleted file mode 100644 index a0a799a..0000000 --- a/src/repositories/objectRepository.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BaseRepository } from '#application/base/baseRepository' -import { UUID } from '#application/types' - -class ObjectRepository extends BaseRepository { - async getById(id: UUID): Promise { - try { - const repository = this.em.getRepository(Object) - return await repository.findOne({ id }) - } catch (error: any) { - return null - } - } - - async getAll(): Promise { - try { - const repository = this.em.getRepository(Object) - return await repository.findAll() - } catch (error: any) { - return null - } - } -} - -export default new ObjectRepository() diff --git a/src/repositories/tileRepository.ts b/src/repositories/tileRepository.ts index b61eb75..172a755 100644 --- a/src/repositories/tileRepository.ts +++ b/src/repositories/tileRepository.ts @@ -4,8 +4,8 @@ import { BaseRepository } from '#application/base/baseRepository' import { UUID } from '#application/types' import { unduplicateArray } from '#application/utilities' import { Tile } from '#entities/tile' -import { Zone } from '#entities/zone' -import ZoneService from '#services/zoneService' +import { Map } from '#entities/map' +import MapService from '#services/mapService' class TileRepository extends BaseRepository { async getById(id: UUID) { @@ -37,18 +37,18 @@ class TileRepository extends BaseRepository { } } - async getByZoneId(zoneId: UUID) { + async getByMapId(mapId: UUID) { try { - const repository = this.em.getRepository(Zone) + const repository = this.em.getRepository(Map) const tileRepository = this.em.getRepository(Tile) - const zone = await repository.findOne({ id: zoneId }) - if (!zone) return [] + const map = await repository.findOne({ id: mapId }) + 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({ - id: zoneTileArray + id: mapTileArray }) } catch (error: any) { return [] diff --git a/src/repositories/zoneEventTileRepository.ts b/src/repositories/zoneEventTileRepository.ts deleted file mode 100644 index 1560d8c..0000000 --- a/src/repositories/zoneEventTileRepository.ts +++ /dev/null @@ -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 { - 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() diff --git a/src/repositories/zoneRepository.ts b/src/repositories/zoneRepository.ts deleted file mode 100644 index e0e9af6..0000000 --- a/src/repositories/zoneRepository.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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() diff --git a/src/server.ts b/src/server.ts index c747ec2..71f0a80 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,7 +13,7 @@ import QueueManager from '#managers/queueManager' import SocketManager from '#managers/socketManager' import UserManager from '#managers/userManager' import WeatherManager from '#managers/weatherManager' -import ZoneManager from '#managers/zoneManager' +import MapManager from '#managers/mapManager' export class Server { private readonly app: Application @@ -45,7 +45,7 @@ export class Server { UserManager.boot(), // DateManager.boot(), // WeatherManager.boot(), - ZoneManager.boot(), + MapManager.boot(), ConsoleManager.boot() ]) } catch (error: any) { diff --git a/src/services/characterService.ts b/src/services/characterService.ts index 5ec478a..188d211 100644 --- a/src/services/characterService.ts +++ b/src/services/characterService.ts @@ -1,11 +1,11 @@ import { BaseService } from '#application/base/baseService' import config from '#application/config' import { Character } from '#entities/character' -import { Zone } from '#entities/zone' +import { Map } from '#entities/map' import SocketManager from '#managers/socketManager' -import ZoneManager from '#managers/zoneManager' +import MapManager from '#managers/mapManager' import CharacterRepository from '#repositories/characterRepository' -import ZoneRepository from '#repositories/zoneRepository' +import MapRepository from '#repositories/mapRepository' type Position = { x: number; y: 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 { - const zone = ZoneManager.getZoneById(character.zone.id) - const grid = await zone?.getGrid() + const map = MapManager.getMapById(character.map.id) + const grid = await map?.getGrid() 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 } diff --git a/src/services/chatService.ts b/src/services/chatService.ts index d29948d..4122a16 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -6,22 +6,22 @@ import { Chat } from '#entities/chat' import SocketManager from '#managers/socketManager' import CharacterRepository from '#repositories/characterRepository' import ChatRepository from '#repositories/chatRepository' -import ZoneRepository from '#repositories/zoneRepository' +import MapRepository from '#repositories/mapRepository' class ChatService extends BaseService { - async sendZoneMessage(characterId: UUID, zoneId: UUID, message: string): Promise { + async sendMapMessage(characterId: UUID, mapId: UUID, message: string): Promise { try { const character = await CharacterRepository.getById(characterId) if (!character) return false - const zone = await ZoneRepository.getById(zoneId) - if (!zone) return false + const map = await MapRepository.getById(mapId) + if (!map) return false 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() - io.to(zoneId).emit('chat:message', chat) + io.to(mapId).emit('chat:message', chat) return true } catch (error: any) { this.logger.error(`Failed to save chat message: ${error instanceof Error ? error.message : String(error)}`) diff --git a/src/services/mapEventTileService.ts b/src/services/mapEventTileService.ts new file mode 100644 index 0000000..06b45ac --- /dev/null +++ b/src/services/mapEventTileService.ts @@ -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 { + 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() diff --git a/src/services/zoneService.ts b/src/services/mapService.ts similarity index 61% rename from src/services/zoneService.ts rename to src/services/mapService.ts index d658017..ed0d17e 100644 --- a/src/services/zoneService.ts +++ b/src/services/mapService.ts @@ -1,7 +1,7 @@ import { BaseService } from '#application/base/baseService' -class ZoneService extends BaseService { - public flattenZoneArray(tiles: string[][]) { +class MapService extends BaseService { + public flattenMapArray(tiles: string[][]) { const normalArray = [] for (const row of tiles) { @@ -12,4 +12,4 @@ class ZoneService extends BaseService { } } -export default new ZoneService() +export default new MapService() diff --git a/src/services/teleportService.ts b/src/services/teleportService.ts index 0c86414..8f4e436 100644 --- a/src/services/teleportService.ts +++ b/src/services/teleportService.ts @@ -2,11 +2,11 @@ import Logger, { LoggerType } from '#application/logger' import { UUID } from '#application/types' import { Character } from '#entities/character' import SocketManager from '#managers/socketManager' -import ZoneManager from '#managers/zoneManager' -import ZoneCharacter from '#models/zoneCharacter' +import MapManager from '#managers/mapManager' +import MapCharacter from '#models/mapCharacter' interface TeleportOptions { - targetZoneId: UUID + targetMapId: UUID targetX: number targetY: number rotation?: number @@ -18,13 +18,13 @@ class TeleportService { private readonly logger = Logger.type(LoggerType.GAME) public async teleportCharacter(characterId: UUID, options: TeleportOptions): Promise { - 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 targetZone = ZoneManager.getZoneById(targetZoneId) + const targetMap = MapManager.getMapById(targetMapId) - if (!socket || !targetZone) { - this.logger.error(`Teleport failed - Missing socket or target zone for character ${characterId}`) + if (!socket || !targetMap) { + this.logger.error(`Teleport failed - Missing socket or target map for character ${characterId}`) return false } @@ -33,40 +33,40 @@ class TeleportService { return false } - const existingCharacter = !isInitialJoin && ZoneManager.getCharacterById(characterId) - const zoneCharacter = isInitialJoin - ? new ZoneCharacter(character!) + const existingCharacter = !isInitialJoin && MapManager.getCharacterById(characterId) + const mapCharacter = isInitialJoin + ? new MapCharacter(character!) : 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 })() - if (!zoneCharacter) return false + if (!mapCharacter) return false try { - const currentZoneId = zoneCharacter.character.zone?.id + const currentMapId = mapCharacter.character.map?.id const io = SocketManager.getIO() - // Handle current zone cleanup - if (currentZoneId) { - socket.leave(currentZoneId) - ZoneManager.removeCharacter(characterId) - io.in(currentZoneId).emit('zone:character:leave', characterId) + // Handle current map cleanup + if (currentMapId) { + socket.leave(currentMapId) + MapManager.removeCharacter(characterId) + io.in(currentMapId).emit('map:character:leave', characterId) } - // Update character position and zone - await zoneCharacter.character.setPositionX(targetX).setPositionY(targetY).setRotation(rotation).setZone(targetZone.getZone()).update() + // Update character position and map + await mapCharacter.character.setPositionX(targetX).setPositionY(targetY).setRotation(rotation).setMap(targetMap.getMap()).update() - // Join new zone - socket.join(targetZoneId) - targetZone.addCharacter(zoneCharacter.character) + // Join new map + socket.join(targetMapId) + targetMap.addCharacter(mapCharacter.character) // Notify clients - io.in(targetZoneId).emit('zone:character:join', zoneCharacter) - socket.emit('zone:character:teleport', { - zone: targetZone.getZone(), - characters: targetZone.getCharactersInZone() + io.in(targetMapId).emit('map:character:join', mapCharacter) + socket.emit('map:character:teleport', { + map: targetMap.getMap(), + characters: targetMap.getCharactersInMap() }) return true diff --git a/src/services/zoneEventTileService.ts b/src/services/zoneEventTileService.ts deleted file mode 100644 index 66fe3a2..0000000 --- a/src/services/zoneEventTileService.ts +++ /dev/null @@ -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 { - 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()