diff --git a/package-lock.json b/package-lock.json index 84eb91c..0551ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -792,9 +792,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "license": "MIT", "bin": { "acorn": "bin/acorn" diff --git a/prisma/migrations/20240629190323_init/migration.sql b/prisma/migrations/20240703230300_init/migration.sql similarity index 93% rename from prisma/migrations/20240629190323_init/migration.sql rename to prisma/migrations/20240703230300_init/migration.sql index 554e876..0f1f8a4 100644 --- a/prisma/migrations/20240629190323_init/migration.sql +++ b/prisma/migrations/20240703230300_init/migration.sql @@ -1,5 +1,5 @@ -- CreateTable -CREATE TABLE `Objects` ( +CREATE TABLE `Object` ( `id` VARCHAR(191) NOT NULL, `name` VARCHAR(191) NOT NULL, `origin_x` INTEGER NOT NULL DEFAULT 0, @@ -61,6 +61,14 @@ CREATE TABLE `CharacterItem` ( PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- CreateTable +CREATE TABLE `TileTag` ( + `tile` VARCHAR(191) NOT NULL, + `tags` JSON NOT NULL, + + PRIMARY KEY (`tile`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- CreateTable CREATE TABLE `Zone` ( `id` INTEGER NOT NULL AUTO_INCREMENT, @@ -113,7 +121,7 @@ ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN K ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_objectId_fkey` FOREIGN KEY (`objectId`) REFERENCES `Objects`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_objectId_fkey` FOREIGN KEY (`objectId`) REFERENCES `Object`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE `Chat` ADD CONSTRAINT `Chat_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 68d3990..4f91978 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,7 +19,7 @@ datasource db { url = env("DATABASE_URL") } -model Objects { +model Object { id String @id @default(uuid()) name String origin_x Int @default(0) @@ -74,6 +74,11 @@ model CharacterItem { quantity Int } +model TileTag { + tile String @id + tags Json +} + model Zone { id Int @id @default(autoincrement()) name String @@ -93,7 +98,7 @@ model ZoneObject { zoneId Int zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade) objectId String - object Objects @relation(fields: [objectId], references: [id]) + object Object @relation(fields: [objectId], references: [id]) position_x Int position_y Int } diff --git a/src/app/events/gm/GmTileList.ts b/src/app/events/gm/objects/GmTileList.ts similarity index 95% rename from src/app/events/gm/GmTileList.ts rename to src/app/events/gm/objects/GmTileList.ts index 54389b6..053c9e9 100644 --- a/src/app/events/gm/GmTileList.ts +++ b/src/app/events/gm/objects/GmTileList.ts @@ -1,5 +1,5 @@ import { Server } from "socket.io"; -import {TSocket} from "../../utilities/Types"; +import {TSocket} from "../../../utilities/Types"; import fs from 'fs'; import path from "path"; diff --git a/src/app/events/gm/GmTileRemove.ts b/src/app/events/gm/objects/GmTileRemove.ts similarity index 94% rename from src/app/events/gm/GmTileRemove.ts rename to src/app/events/gm/objects/GmTileRemove.ts index 4ca7988..6b2bc33 100644 --- a/src/app/events/gm/GmTileRemove.ts +++ b/src/app/events/gm/objects/GmTileRemove.ts @@ -1,5 +1,5 @@ import { Server } from "socket.io"; -import {TSocket} from "../../utilities/Types"; +import {TSocket} from "../../../utilities/Types"; import {writeFile} from "node:fs"; import {randomUUID} from "node:crypto"; import path from "path"; diff --git a/src/app/events/gm/objects/GmTileTags.ts b/src/app/events/gm/objects/GmTileTags.ts new file mode 100644 index 0000000..c5c2579 --- /dev/null +++ b/src/app/events/gm/objects/GmTileTags.ts @@ -0,0 +1,30 @@ +import { Server } from "socket.io"; +import {TSocket} from "../../../utilities/Types"; +import TileTagRepository from "../../../repositories/TileTagRepository"; + +interface IPayload { + tile: string; +} + +/** + * Handle game master tile tags update event + * @param socket + * @param io + */ +export default function (socket: TSocket, io: Server) { + socket.on('gm:tile:tags', async (data: IPayload, callback: (response: string[]) => void) => { + + if (socket.character?.role !== 'gm') { + return; + } + + // update the tile tags + try { + const tileTag = await TileTagRepository.getTileTag(data.tile); + callback(tileTag ? tileTag.tags as string[] : []); + } catch (error) { + console.log(error); + callback([]); + } + }); +} \ No newline at end of file diff --git a/src/app/events/gm/objects/GmTileTagsUpdate.ts b/src/app/events/gm/objects/GmTileTagsUpdate.ts new file mode 100644 index 0000000..426e0af --- /dev/null +++ b/src/app/events/gm/objects/GmTileTagsUpdate.ts @@ -0,0 +1,31 @@ +import { Server } from "socket.io"; +import {TSocket} from "../../../utilities/Types"; +import TileTagRepository from "../../../repositories/TileTagRepository"; + +interface IPayload { + tile: string; + tags: string[]; +} + +/** + * Handle game master tile tags update event + * @param socket + * @param io + */ +export default function (socket: TSocket, io: Server) { + socket.on('gm:tile:tags:update', async (data: IPayload, callback: (response: boolean) => void) => { + + if (socket.character?.role !== 'gm') { + return; + } + + // update the tile tags + try { + await TileTagRepository.upsertTileTag(data.tile, data.tags); + callback(true); + } catch (error) { + console.log(error); + callback(false); + } + }); +} \ No newline at end of file diff --git a/src/app/events/gm/GmTileUpload.ts b/src/app/events/gm/objects/GmTileUpload.ts similarity index 96% rename from src/app/events/gm/GmTileUpload.ts rename to src/app/events/gm/objects/GmTileUpload.ts index a04884c..7207ab6 100644 --- a/src/app/events/gm/GmTileUpload.ts +++ b/src/app/events/gm/objects/GmTileUpload.ts @@ -1,5 +1,5 @@ import { Server } from "socket.io"; -import { TSocket } from "../../utilities/Types"; +import { TSocket } from "../../../utilities/Types"; import { writeFile } from "node:fs/promises"; import path from "path"; import fs from "fs/promises"; diff --git a/src/app/events/gm/tiles/GmObjectDetails.ts b/src/app/events/gm/tiles/GmObjectDetails.ts new file mode 100644 index 0000000..f67d347 --- /dev/null +++ b/src/app/events/gm/tiles/GmObjectDetails.ts @@ -0,0 +1,33 @@ +import { Server } from "socket.io"; +import {TSocket} from "../../../utilities/Types"; +import ObjectRepository from '../../../repositories/ObjectRepository' +import { Object } from '@prisma/client' + +interface IPayload { + object: string; +} + +// callback will return Object from Prisma +type TCallback = (object: Object | null) => void; + +/** + * Handle game master object details fetch event + * @param socket + * @param io + */ +export default function (socket: TSocket, io: Server) { + socket.on('gm:object:details', async (data: IPayload, callback: TCallback) => { + + if (socket.character?.role !== 'gm') { + return; + } + + try { + const object = await ObjectRepository.getById(data.object); + callback(object); + } catch (error) { + console.error(error); + callback(null); + } + }); +} \ No newline at end of file diff --git a/src/app/events/gm/tiles/GmObjectList.ts b/src/app/events/gm/tiles/GmObjectList.ts new file mode 100644 index 0000000..0b7a893 --- /dev/null +++ b/src/app/events/gm/tiles/GmObjectList.ts @@ -0,0 +1,42 @@ +import { Server } from "socket.io"; +import {TSocket} from "../../../utilities/Types"; +import fs from 'fs'; +import path from "path"; + +interface IPayload { +} + +/** + * Handle game master list objects event + * @param socket + * @param io + */ +export default function (socket: TSocket, io: Server) { + socket.on('gm:object:list', async (data: any, callback: (response: string[]) => void) => { + + if (socket.character?.role !== 'gm') { + console.log(`---Character #${socket.character?.id} is not a game master.`); + return; + } + + // get root path + const folder = path.join(process.cwd(), 'public', 'objects'); + + // list the files in the folder + let objects: string[] = []; + + fs.readdir(folder, (err, files) => { + if (err) { + console.log(err); + return; + } + + files.forEach(file => { + objects.push(file.replace('.png', '')); + }); + + // send over the list of objects to the socket + callback(objects); + }); + }); +} \ No newline at end of file diff --git a/src/app/events/gm/tiles/GmObjectRemove.ts b/src/app/events/gm/tiles/GmObjectRemove.ts new file mode 100644 index 0000000..28ccbea --- /dev/null +++ b/src/app/events/gm/tiles/GmObjectRemove.ts @@ -0,0 +1,39 @@ +import { Server } from "socket.io"; +import {TSocket} from "../../../utilities/Types"; +import {writeFile} from "node:fs"; +import {randomUUID} from "node:crypto"; +import path from "path"; +import fs from "fs"; + +interface IPayload { + object: string; +} + +/** + * Handle game master remove object event + * @param socket + * @param io + */ +export default function (socket: TSocket, io: Server) { + socket.on('gm:object:remove', async (data: IPayload, callback: (response: boolean) => void) => { + + if (socket.character?.role !== 'gm') { + return; + } + + // get root path + const public_folder = path.join(process.cwd(), 'public', 'objects'); + + // remove the tile from the disk + const finalFilePath = path.join(public_folder, data.object); + fs.unlink(finalFilePath, (err) => { + if (err) { + console.log(err); + callback(false); + return; + } + + callback(true); + }); + }); +} \ No newline at end of file diff --git a/src/app/events/gm/tiles/GmObjectUpload.ts b/src/app/events/gm/tiles/GmObjectUpload.ts new file mode 100644 index 0000000..642231a --- /dev/null +++ b/src/app/events/gm/tiles/GmObjectUpload.ts @@ -0,0 +1,45 @@ +import { Server } from "socket.io"; +import { TSocket } from "../../../utilities/Types"; +import { writeFile } from "node:fs/promises"; +import path from "path"; +import fs from "fs/promises"; +import { randomUUID } from 'node:crypto'; + +interface IObjectData { + [key: string]: Buffer; +} + +/** + * Handle game master upload object event + * @param socket + * @param io + */ +export default function (socket: TSocket, io: Server) { + socket.on('gm:object:upload', async (data: IObjectData, callback: (response: boolean) => void) => { + try { + if (socket.character?.role !== 'gm') { + callback(false); + return; + } + + const public_folder = path.join(process.cwd(), 'public', 'objects'); + + // Ensure the folder exists + await fs.mkdir(public_folder, { recursive: true }); + + const uploadPromises = Object.entries(data).map(async ([key, objectData]) => { + const uuid = randomUUID(); + const filename = `${uuid}.png`; + const finalFilePath = path.join(public_folder, filename); + await writeFile(finalFilePath, objectData); + }); + + await Promise.all(uploadPromises); + + callback(true); + } catch (error) { + console.error('Error uploading objects:', error); + callback(false); + } + }); +} \ No newline at end of file diff --git a/src/app/repositories/ObjectRepository.ts b/src/app/repositories/ObjectRepository.ts new file mode 100644 index 0000000..e8fa5fc --- /dev/null +++ b/src/app/repositories/ObjectRepository.ts @@ -0,0 +1,14 @@ +import prisma from '../utilities/Prisma'; // Import the global Prisma instance +import { Object } from '@prisma/client' + +class ObjectRepository { + getById(id: string): Promise { + return prisma.object.findUnique({ + where: { + id, + }, + }); + } +} + +export default new ObjectRepository(); \ No newline at end of file diff --git a/src/app/repositories/TileTagRepository.ts b/src/app/repositories/TileTagRepository.ts new file mode 100644 index 0000000..34b7d20 --- /dev/null +++ b/src/app/repositories/TileTagRepository.ts @@ -0,0 +1,45 @@ +import prisma from '../utilities/Prisma'; // Import the global Prisma instance +import { TileTag } from '@prisma/client' + +class TileTagRepository { + async upsertTileTag(tile: string, tags: string[]): Promise { + return prisma.tileTag.upsert({ + where: { tile }, + create: { + tile, + tags: tags, + }, + update: { + tags: tags, + }, + }); + } + + async getTileTag(tile: string): Promise { + return prisma.tileTag.findUnique({ + where: { tile }, + }); + } + + async deleteTileTag(tile: string): Promise { + return prisma.tileTag.delete({ + where: { tile }, + }); + } + + async searchTilesByTags(tags: string[]): Promise { + return prisma.tileTag.findMany({ + where: { + tags: { + array_contains: tags, + } as any, // Type assertion needed due to Json field + }, + }); + } + + async getAllTileTags(): Promise { + return prisma.tileTag.findMany(); + } +} + +export default new TileTagRepository(); \ No newline at end of file diff --git a/src/app/services/AssetService.ts b/src/app/services/AssetService.ts index afbb1e6..24158dc 100644 --- a/src/app/services/AssetService.ts +++ b/src/app/services/AssetService.ts @@ -1,9 +1,6 @@ class AssetService { - static generateTileset() { - - } } export default AssetService; \ No newline at end of file diff --git a/src/app/utilities/Http.ts b/src/app/utilities/Http.ts index 29ee708..a857a97 100644 --- a/src/app/utilities/Http.ts +++ b/src/app/utilities/Http.ts @@ -18,6 +18,12 @@ async function addHttpRoutes(app: Application) { tiles.forEach(tile => { assets.push({key: tile, value: '/tiles/' + tile, group: 'tiles', type: 'link'}); }); + + const objects = listObjects(); + objects.forEach(object => { + assets.push({key: object, value: '/objects/' + object, group: 'objects', type: 'link'}); + }); + res.json(assets); }); app.get('/assets/:type/:file', (req: Request, res: Response) => { @@ -104,4 +110,26 @@ function listTiles(): string[] { console.log(tiles); return tiles; +} + +function listObjects(): string[] { + // get root path + const folder = path.join(process.cwd(), 'public', 'objects'); + + // list the files in the folder + let objects: string[] = []; + + try { + const files = fs.readdirSync(folder); + + files.forEach(file => { + objects.push(file.replace('.png', '')); + }); + } catch (err) { + console.log(err); + } + + console.log(objects); + + return objects; } \ No newline at end of file