1
0
forked from noxious/server

Compare commits

...

242 Commits

Author SHA1 Message Date
5ac056bb8a Cleaned package.json, removed old code, temp. disabled mikro orm debugging 2025-01-02 00:32:07 +01:00
664a74c973 Characters bug fix 2025-01-01 23:52:48 +01:00
0c77758351 More small improvements 2025-01-01 23:01:44 +01:00
e8ef160f2a Minor improvements 2025-01-01 22:50:58 +01:00
2d6831b4ef Improved readability of weather and date managers 2025-01-01 22:09:44 +01:00
0464538b1c N/A 2025-01-01 21:57:47 +01:00
e61aeed691 Refactor send chat logic 2025-01-01 21:57:24 +01:00
45e756fcd3 Fixed left-overs from #293 2025-01-01 21:49:01 +01:00
7c473de12b number>uuid 2025-01-01 21:37:02 +01:00
5982422e04 Storage class is now OOP 2025-01-01 21:34:23 +01:00
04e081c31a Small improvement 2025-01-01 21:19:35 +01:00
599264b362 TS fix 2025-01-01 21:03:42 +01:00
6f84238503 Minor TS improvement 2025-01-01 20:59:30 +01:00
586bb0ca83 #293: Changed IDs to UUIDs for all entities 2025-01-01 20:53:05 +01:00
465219276d Added update & delete rules to entities 2025-01-01 20:44:36 +01:00
11d30351ba Bug fix for loading sprite actions 2025-01-01 20:44:16 +01:00
85af73c079 renamed id to characterId 2025-01-01 17:49:10 +01:00
9c28b10383 format & lint 2025-01-01 04:48:30 +01:00
495e9f192e Joining, leaving rooms and teleporting works again + refactor 2025-01-01 04:46:00 +01:00
30b2028bd8 Fix for creating new characters, added teleport function to zone character model 2025-01-01 03:00:03 +01:00
9d6a8730a9 Added socket helper functions 2024-12-31 15:27:31 +01:00
ba12674e7c Moved more socket logic into socket manager for easier DX 2024-12-30 02:39:31 +01:00
a5c941cbb0 Added note, added new init migration 2024-12-30 01:37:49 +01:00
0f017cfe10 Removed Prisma related files 2024-12-29 13:05:56 +01:00
c9cc5be519 Minor improvement for server.start() 2024-12-29 02:38:22 +01:00
ce073a67af Added socketManager and moved logic into it where appropiate 2024-12-29 02:35:50 +01:00
cb6fcbcb8e Maybe 3 2024-12-29 01:43:24 +01:00
ba0ceda03a Maybe 2 2024-12-29 01:30:06 +01:00
4745fb5145 maybe 2024-12-29 01:26:24 +01:00
30cce5845c Removed config variable 2024-12-29 00:42:18 +01:00
2d43b3f1d2 WIP zone loading 2024-12-29 00:40:11 +01:00
045c693329 Removed characterJoin and characterLeave 2024-12-28 23:48:37 +01:00
ff01e41c8f Moved funcs to basEvent 2024-12-28 23:15:00 +01:00
8781bf43a1 a 2024-12-28 22:23:04 +01:00
1223ae42ef Updated socket middleware 2024-12-28 22:19:08 +01:00
6e58de8840 Improvements to weatherManager 2024-12-28 22:17:57 +01:00
f5c4f3df19 httpManager now matches manager format 2024-12-28 22:16:30 +01:00
e1ff2fefe1 Renamed folder 2024-12-28 22:12:59 +01:00
0bc81ba4cc Updated init command 2024-12-28 22:11:54 +01:00
baf0350102 Router is now httpManager 2024-12-28 21:33:32 +01:00
5c94584cb2 format 2024-12-28 21:26:31 +01:00
4f1b9cf024 Renamed command manager to console manager, improved log reading 2024-12-28 21:24:59 +01:00
0b99d4098e Better streamlined naming in socket events 2024-12-28 20:57:48 +01:00
5b386ae455 Updated more socket events 2024-12-28 20:51:11 +01:00
3da21a7856 Redundant code 2024-12-28 20:41:53 +01:00
e1a6f650fb Continuation of refactor 2024-12-28 20:40:05 +01:00
6dda79f8b2 MORE IMPROVEMENTS 2024-12-28 19:31:47 +01:00
918f5141fc Many many more improvements 2024-12-28 19:21:15 +01:00
bd3bf6f580 Greatly improved server code base 2024-12-28 17:26:17 +01:00
bd85908014 DB init. improvements 2024-12-27 19:03:05 +01:00
474683082d packages 2024-12-27 16:06:04 +01:00
0fe060ff99 More improvements 2024-12-27 03:36:45 +01:00
30dc69ab4b Added ESLint 2024-12-27 02:50:11 +01:00
343c67a110 Fixes 2024-12-27 02:44:32 +01:00
5d6cb478cd Removed redundant code pt2. 2024-12-26 23:54:19 +01:00
a95c67b5fe Removed redundant code 2024-12-26 23:50:04 +01:00
e571cf2230 Converted more procedural programming to OOP 2024-12-26 23:34:25 +01:00
b7f448cb17 Started improving character move event 2024-12-26 17:45:14 +01:00
4a963b4359 Improved entities, ran formatting, utilise getters and setters 2024-12-26 16:45:00 +01:00
691abb7c4f Added getter & setter functions to all entities 2024-12-26 15:15:18 +01:00
6a27ccff31 Init command works 2024-12-26 04:21:35 +01:00
9d72995225 Reverted .env.example, updated init script 2024-12-26 01:21:43 +01:00
2de2bec705 Added README.md, started refactoring init. command 2024-12-25 19:07:03 +01:00
bf64a6df70 Authentication works again 2024-12-25 17:53:59 +01:00
1b87f1dd91 Minor repo improvement 2024-12-25 17:28:35 +01:00
f4746722af Renamed folder utilities > application, added baseEntity class, updated baseRepo class, removed prisma helper 2024-12-25 16:50:01 +01:00
f5a7a348e0 Improvements in asset http endpoints 2024-12-25 14:13:53 +01:00
d70e25207b All repositories use MikroORM now 2024-12-25 14:09:46 +01:00
4dd71a25b5 Refactor getByZoneId() in tile repo 2024-12-25 13:57:51 +01:00
5c87b7b4af Typo 2024-12-25 13:51:25 +01:00
aa3ee8f0af Enum bug fix 2024-12-25 13:49:43 +01:00
95f4c58110 Added missing entities( zoneEffect, zoneEventTile, zoneEventTileTeleport, zoneObject) 2024-12-25 03:19:53 +01:00
b4989aac26 npm run format 2024-12-25 02:06:58 +01:00
c35e27799e Fixed enum fields, new migration file 2024-12-25 02:06:02 +01:00
125d3a3f66 Added init migration, moved ORM config into separate file, updated .gitignore file 2024-12-25 01:16:07 +01:00
d299528c26 Added base repository, updated char. hair repo and user repo 2024-12-25 01:15:30 +01:00
bc58d41c54 Added back node-ts for MikroORM 2024-12-25 01:10:49 +01:00
058988e874 Added paths in typescript config and utilise this in all project files 2024-12-24 23:48:37 +01:00
72562f92f9 Updated user repository to use MikroORM 2024-12-24 23:29:17 +01:00
38395b6f19 Better imports 2024-12-24 23:16:13 +01:00
88cc8f5b08 Added comment 2024-12-24 23:16:05 +01:00
413a5cbcf5 Made database class to call entity manager with 2024-12-24 23:14:08 +01:00
cfae96bde8 Updated entities 2024-12-24 22:56:12 +01:00
42e7b7312e Updated .gitignore 2024-12-24 22:28:07 +01:00
f76b758565 Added @mikro-orm/reflection 2024-12-24 22:25:18 +01:00
434f5df7c2 Typo 2024-12-24 22:19:55 +01:00
5990b6d6ac Added enums, moved Mikro ORM config into server.ts, cleaned configuration and env files 2024-12-24 22:17:47 +01:00
8377fe6545 Added enums, moved Mikro ORM config into server.ts, cleaned configuration and env files 2024-12-24 22:17:41 +01:00
8980691409 Added ORM entities 2024-12-24 22:08:08 +01:00
40c24cee10 Added Mikro ORM config 2024-12-24 22:03:20 +01:00
e5df80647f Replaced ts-node with tsx 2024-12-24 22:01:11 +01:00
75595515b5 Improved typescript config 2024-12-24 22:00:53 +01:00
e19f30b15a Updated tsconfig.json according to MikroORM documentation 2024-12-24 16:04:48 +01:00
af5f4f97f2 Added MikroORM to packages, fixed character repository before swap 2024-12-24 15:56:26 +01:00
241cfb3eb2 famous last words: fuck (changing ORM) 2024-12-24 15:27:24 +01:00
ac4cefa902 Copy sprite event listener and logic 2024-12-24 00:54:43 +01:00
3dffad928d Removed redundant line of code 2024-12-22 23:44:55 +01:00
ce90d6f7a9 Removed check that prevented overwriting existing spritesheets 2024-12-22 23:33:25 +01:00
b520acc2db Updated assets 2024-12-22 22:01:09 +01:00
75333d2659 #289: Add init command for easy installation 2024-12-22 21:51:33 +01:00
dd9e039649 CRUD for items 2024-12-22 20:09:14 +01:00
1facd2d641 Improvements to spritesheet gen. 2024-12-22 02:32:06 +01:00
5af2e399c9 Added comment for #287\ 2024-12-22 01:32:00 +01:00
6e3c97d7d1 Added padding to character 2024-12-22 01:18:18 +01:00
bf58fc4944 #16: Working PoC avatar image generator 2024-12-22 00:12:43 +01:00
68f7db7aa4 Restored old endpoint slugs 2024-12-21 23:09:07 +01:00
bbcd122a6c Potential fix? 2024-12-21 23:08:16 +01:00
23c437f0bc npm run format 2024-12-21 22:51:08 +01:00
eb2648d31f Added extra HTTP boilerplate allowing us to have endpoints in separated files 2024-12-21 22:45:17 +01:00
7e8fcc766a Minor improvements 2024-12-21 02:59:44 +01:00
5c47edd230 Renamed frameSpeed > frameRate 2024-12-21 02:27:14 +01:00
2c10b54582 #262 : Send frameRate to client 2024-12-21 02:20:14 +01:00
2ad58aea9f renamed isEnabledForCharCreation > isSelectable 2024-12-21 02:13:06 +01:00
bd8caaf27c POC 2 2024-12-20 23:51:39 +01:00
3cf86e322c proof of concept 2024-12-20 23:44:13 +01:00
6f32fbdc79 Improved readability 2024-12-20 23:36:56 +01:00
743d4594df 🎃 2024-12-20 23:13:55 +01:00
2be49c010f Horror code 2024-12-20 21:29:19 +01:00
2ac9416fe6 senior dev moment, finished spritesheet generator 2024-12-20 20:34:39 +01:00
43fe6ab33e Better sprite-sheet generating 2024-12-20 01:28:36 +01:00
1cbf116ad4 🤠🔫 2024-12-18 03:05:11 +01:00
3f10b03d24 Improved table names 2024-12-17 17:07:56 +01:00
a525d80530 New migration file 2024-12-17 17:01:10 +01:00
4748044ab3 Added equipment tables and columns in game & user schema 2024-12-17 16:59:52 +01:00
3b0138130b Removed extra lines that made no differenec 2024-12-15 21:16:33 +01:00
54c75896f9 Put all images in a container size as big as the biggest sprite that was found 2024-12-15 20:57:54 +01:00
9467797dc9 Aids 2024-12-15 20:25:30 +01:00
a8934f8e40 Spritesheet generator improvements 2024-12-15 17:39:47 +01:00
65cae5d824 Spritesheet gen. optimisations 2024-12-15 14:41:00 +01:00
179ccdbc55 Improved texture generation 2024-12-12 00:43:34 +01:00
ff39628f0c Solved teleporting issue 2024-12-07 23:06:59 +01:00
d4680b198e npm update 2024-12-05 09:56:28 +01:00
550b961505 Removed func. 2024-11-24 20:11:52 +01:00
1839bd9a22 Renamed hair > characterHair 2024-11-24 15:13:28 +01:00
1017013032 Moved service logic from repo to service, minor improvements, working hair customisation proof of concept 2024-11-23 16:48:07 +01:00
d5c7cd0294 Added CRUD logic for character hair, made some minor improvements, npm update 2024-11-23 15:30:11 +01:00
4a62bbb118 #245 & #254: Started working on character hair sprite management & character customisation 2024-11-21 02:58:25 +01:00
40c7f6289a npm update 2024-11-20 21:19:42 +01:00
72d731c6f2 OOP improvement 2024-11-17 21:21:34 +01:00
fc92d9ea79 Updated create and update calls to services 2024-11-17 20:59:24 +01:00
2e267a36aa #237 - Changed Prisma find calls to repos 2024-11-17 19:48:00 +01:00
6ee8bb8334 Minor improvements for char. type management 2024-11-17 18:00:58 +01:00
ec3bf0f51e Moved models to simplify and improve prisma schemas , added logic to update characterTypes 2024-11-17 17:53:25 +01:00
27f8bc8784 Implemented ZoneManager logics in teleport to make sure chars. are removed / added correctly 2024-11-16 23:12:37 +01:00
3185c478a6 Merge branch 'main' of ssh://gitea.directonline.io:29417/sylvan-quest/server 2024-11-16 01:21:35 +01:00
72ef04d683 Rm. comment. 2024-11-16 01:21:32 +01:00
446e8fa617 #237 Convert prisma finds to repos for time and data 2024-11-15 21:58:08 +01:00
f7072acdd2 Added code comment for #246 2024-11-15 01:58:05 +01:00
fda8cc532e #154: Remove tiles, event tiles, and objects that are out of the zone's width and height before saving zone 2024-11-14 23:46:26 +01:00
821e742527 Bug fix 2024-11-14 21:03:08 +01:00
460308d555 #161: Set default value for createdAt field, store zone chats into database 2024-11-14 20:45:37 +01:00
3f8f8745eb #161: Set default value for createdAt field, store zone chats into database 2024-11-14 20:43:40 +01:00
bf7f585270 Path walk improvements 2024-11-13 17:02:16 +01:00
fee4277b4f Removed emit 2024-11-13 16:10:41 +01:00
ffc07b7403 Send title with connect error message 2024-11-13 16:07:17 +01:00
344ddbaf39 #213: Prevent character:connect if user is already logged in from another character 2024-11-13 16:06:31 +01:00
86ed3ae4b0 Minor improvements 2024-11-13 15:34:43 +01:00
719c75616e CORS fix 2024-11-13 13:35:41 +01:00
cf954979c5 Improved logging some more 2024-11-13 13:27:44 +01:00
01ed1bce29 #233: Replaced all console logs, thrown errors with logger API 2024-11-13 13:25:03 +01:00
d4e0cbe398 #174: Refactor character manager into zoneManager for better DX, major refactor of time and weather system (data is stored in DB now instead of JSON file), npm update, npm format, many other improvements 2024-11-13 13:21:01 +01:00
628b3bf1fa npm update 2024-11-05 23:17:59 +01:00
709d34d59b npm run format 2024-11-05 23:16:18 +01:00
c4a42066ab #184: Added commands to toggle rain / fog, command bug fix, minor improvements 2024-11-05 23:15:56 +01:00
26dbaa45a7 #184: Allow GMs to set time 2024-11-05 23:07:23 +01:00
ae0241fecb #184: Added weather manager that periodically changes the weather in-game and emits this to players 2024-11-05 22:45:57 +01:00
3a566dae5a CORS fix 2024-11-05 21:48:55 +01:00
ad4f33676f Bug fix for reset password 2024-11-05 20:32:27 +01:00
44cfbd6ee8 Improved error message to client for password reset request, delete PasswordTokenReset after changing password 2024-11-05 01:00:11 +01:00
881e3375ab Better naming 2024-11-05 00:37:38 +01:00
6a76c4797a Added extra logging to HTTP endpoints 2024-11-05 00:33:12 +01:00
bf75ad001b Typo 2024-11-05 00:28:27 +01:00
3b473e5826 Added client URL to .env.example and cors 2024-11-05 00:27:09 +01:00
929a36554a Cors update 2024-11-05 00:19:02 +01:00
3fbc5f4e87 -_- 2024-11-05 00:15:20 +01:00
1526e0947a Added urlToken to Zod schema 2024-11-04 22:36:00 +01:00
7ec4303b40 npm format 2024-11-04 01:05:21 +01:00
f475b69022 npm update 2024-11-03 22:35:33 +01:00
27d8c7cff6 Finish password reset (hopefully) 2024-11-03 21:54:54 +01:00
b9a7f9aa8e Typo 2024-11-03 01:33:43 +01:00
5b6b968541 Merge remote-tracking branch 'origin/feature/#182-reset-password' 2024-11-03 01:27:46 +01:00
93abf4b631 Updated token hash, use repo instead of prisma for data fetching 2024-11-03 00:50:00 +01:00
0de574b9e1 npm update 2024-11-03 00:33:06 +01:00
c04c52aed0 Merge remote-tracking branch 'origin/main' into feature/#182-reset-password
# Conflicts:
#	src/utilities/http.ts
2024-11-02 21:43:25 +01:00
3f19730bd8 Added param to JSdoc 2024-11-02 21:26:47 +01:00
d0e3c95bb0 npm update 2024-11-02 02:18:17 +01:00
82f51b2b7e Added pw token expiry check, temporarily commented mailer code due to bugs 2024-11-02 01:46:50 +01:00
1b9db64854 npm update 2024-10-31 12:31:40 +01:00
41c71d5964 Added list_sprite_actions http endpoint 2024-10-30 15:26:39 +01:00
bd04dc2ab8 Continuation dynamic asset loading 2024-10-30 09:34:07 +01:00
a4e96f9ede (WIP) Added pw reset token row, added checks to reset function 2024-10-29 22:49:21 +01:00
f6bac403a2 Minor changes 2024-10-28 23:41:54 +01:00
8460d0b535 Worked on http endpoints for dynamic tile loading 2024-10-28 23:23:10 +01:00
5a36d10f0e Added reset password function + basic mail layout 2024-10-27 21:30:33 +01:00
8f8f019ab7 Add email field and add it to register logic 2024-10-27 17:25:45 +01:00
6a1823586a Commented out http endpoint 2024-10-26 02:41:41 +02:00
9d08073fa8 npm update, http asset endpoint changes 2024-10-25 22:21:06 +02:00
5631930bf5 Typo fix¿ 2024-10-21 19:13:20 +02:00
b6e7a5d7fe Started working on Dexie support 2024-10-21 02:08:04 +02:00
63804336be Inform user about not meeting requirements upon character creation 2024-10-19 23:39:56 +02:00
0b62b4231b npm update 2024-10-19 21:15:26 +02:00
4e1e7d95ac Added logging, worked on character type management 2024-10-19 02:14:39 +02:00
d29420cbf3 ? 2024-10-18 23:56:53 +02:00
acc04daa27 New migration 2024-10-18 23:24:43 +02:00
8abf5acef3 #137 : ZoneEffects 2024-10-18 23:08:50 +02:00
780cac9644 npm update 2024-10-18 00:21:38 +02:00
44481e19a8 npm update 2024-10-17 02:32:17 +02:00
9075bfaad5 npm update 2024-10-16 15:45:49 +02:00
bfd941c091 Rnamed Datetime > Date 2024-10-14 20:11:08 +02:00
bb9f62a9c8 Renamed files to storage, re-worked datetimeManager, added json help utilities 2024-10-14 19:47:52 +02:00
049b9de2b3 Renamed utilities to files, added datetimeManager, npm update 2024-10-13 12:15:29 +02:00
2008646a3f npm update 2024-10-04 20:06:09 +02:00
075592702c Zone editor bug fix: set updatedAt at saving so it gets sent back to the client 2024-10-02 19:57:08 +02:00
d271efc1ec npm update 2024-10-02 16:17:43 +02:00
297d4742a4 #91 : Zone editor: allow objects to be rotated 2024-10-01 21:59:51 +02:00
ab649b9fa1 Removed debugging line 2024-10-01 00:34:58 +02:00
4f643269eb Replace fix for tiles command 2024-10-01 00:30:25 +02:00
ce1708a55e Path fixes for all environments, npm run format, removed redundant imports 2024-10-01 00:10:30 +02:00
4cbd62cbb0 Test #1 2024-09-30 23:31:00 +02:00
7b3c4b92a5 File loading fixes for prod. 2024-09-30 23:18:27 +02:00
da8ef9fa65 Uhm excuse me, but what the fuck 2024-09-30 22:56:17 +02:00
4f9a1bc879 npm run dev 2024-09-30 22:42:46 +02:00
3638e2a793 Fixed oopsies 2024-09-30 22:39:48 +02:00
6ac827630a Update command manager and commands to OOP 2024-09-30 22:29:58 +02:00
3ec4bc2557 Prod. command fix attempt #2 2024-09-30 22:20:07 +02:00
6a286590b4 Fix maybe 2024-09-30 22:17:14 +02:00
34ed2ba7cb TMUX? 2024-09-30 21:23:38 +02:00
72159cdc17 Add screen 2024-09-30 21:14:19 +02:00
70d8c43350 #169 : Fixed command for tile bleeding 2024-09-30 20:22:41 +02:00
3a83f2c1ff #169 : Re-enabled command manager, created extrudeTiles command for testing 2024-09-30 19:02:55 +02:00
ddeee356b4 Revert "#169 : Expand tiles to 68x64px and draw 1px line to each side-profile to"
This reverts commit e9fb277d63.
2024-09-29 21:56:03 +02:00
e9fb277d63 #169 : Expand tiles to 68x64px and draw 1px line to each side-profile to 2024-09-29 21:23:16 +02:00
e8aee51248 #95 : Replaced : with / for commands 2024-09-28 21:26:27 +02:00
46fdb3edb6 #160 : Fix for being unable to chat after teleporting into another zone 2024-09-28 03:17:15 +02:00
dec6b36699 #143 : Fix switching back to zone from zoneEditor 2024-09-28 02:52:16 +02:00
21a75f6cbe npm run format 2024-09-28 02:18:31 +02:00
10a231b54c Log when a character joins a zone 2024-09-28 02:18:06 +02:00
cc9eada654 Send frameCount for assets to client 2024-09-28 00:59:20 +02:00
6f057639c0 npm update 2024-09-28 00:39:05 +02:00
251a72aa97 npm update 2024-09-26 00:42:38 +02:00
0d7ed18b03 npm update 2024-09-24 16:09:40 +02:00
4a9b7987dc Added option to set rotation on teleport tiles, new base database migration (db reset needed) 2024-09-23 14:02:25 +02:00
163 changed files with 11859 additions and 2978 deletions

View File

@ -1,9 +1,17 @@
# Server configuration
ENV=development
HOST="0.0.0.0"
PORT=4000
DATABASE_URL="mysql://root@localhost:3306/nq"
REDIS_URL="redis://@127.0.0.1:6379/4"
JWT_SECRET="secret"
CLIENT_URL="http://192.168.3.4:5173"
# Database configuration
REDIS_URL="redis://@127.0.0.1:6379/4"
DB_HOST="localhost"
DB_USER="root"
DB_PASS=""
DB_PORT="3306"
DB_NAME="game"
# Game configuration
ALLOW_DIAGONAL_MOVEMENT=false
@ -11,4 +19,10 @@ ALLOW_DIAGONAL_MOVEMENT=false
# Default character create values
DEFAULT_CHARACTER_ZONE="0"
DEFAULT_CHARACTER_POS_X="0"
DEFAULT_CHARACTER_POS_Y="0"
DEFAULT_CHARACTER_POS_Y="0"
# Email configuration
SMTP_HOST=my.directonline.io
SMTP_PORT=587
SMTP_USER=no-reply@noxious.gg
SMTP_PASSWORD=""

7
.gitignore vendored
View File

@ -309,7 +309,8 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows
# MikroORM
temp/**
migrations/*.json
prisma/dev.db
prisma/dev.db-journal
# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows

View File

@ -1,8 +1,8 @@
# Use the official Node.js 22.4.1 image
FROM node:22.4.1-alpine
# Install Redis
RUN apk add --no-cache redis
# Install Redis and tmux
RUN apk add --no-cache redis tmux
# Set the working directory in the container
WORKDIR /usr/src/
@ -28,11 +28,13 @@ RUN npm run build
# Expose the ports your Node.js application and Redis will listen on
EXPOSE 80 6379
# Create a shell script to run Redis, run migrations, and start the application
# Create a shell script to run Redis, run migrations, and start the application in a tmux session
RUN echo '#!/bin/sh' > /usr/src/start.sh && \
echo 'redis-server --daemonize yes' >> /usr/src/start.sh && \
echo 'npx prisma migrate deploy' >> /usr/src/start.sh && \
echo 'node dist/server.js' >> /usr/src/start.sh && \
echo 'tmux new-session -d -s nodeapp "node dist/server.js"' >> /usr/src/start.sh && \
echo 'echo "App is running in tmux session. Attach with: tmux attach-session -t nodeapp"' >> /usr/src/start.sh && \
echo 'tail -f /dev/null' >> /usr/src/start.sh && \
chmod +x /usr/src/start.sh
# Use the shell script as the entry point

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# Noxious game server
This is the server for the Noxious game.
## Installation
1. Clone the repository
2. Install dependencies with `npm install`
3. Copy the `.env.example` file to `.env` and fill in the required variables
4. Run the server with `npm run dev`
## Commands
### `npm run dev`
Starts the server in development mode.
### `npm run build`
Builds the server for production.
### `npm run format`
Formats the code using Prettier.
## MikroORM
MikroORM is used as the ORM for the server.
### Create init. migrations
Run `npx mikro-orm migration:create --initial` to create a new initial migration.
### Create migrations
Run `npx mikro-orm migration:create` to create a new migration.
### Apply migrations
Run `npx mikro-orm migration:up` to apply all pending migrations.

42
eslint.config.js Normal file
View File

@ -0,0 +1,42 @@
import eslint from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
export default [
eslint.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2023,
},
},
plugins: {
'@typescript-eslint': tseslint,
'import': importPlugin,
},
rules: {
...tseslint.configs['recommended'].rules,
...tseslint.configs['recommended-requiring-type-checking'].rules,
'import/order': ['error', {
'groups': [
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
'object',
'type'
],
'newlines-between': 'always',
'alphabetize': {
'order': 'asc',
'caseInsensitive': true
}
}]
}
}
];

View File

@ -0,0 +1,104 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250101224501 extends Migration {
override async up(): Promise<void> {
this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` int not null default 0, \`origin_y\` int not null default 0, \`is_animated\` tinyint(1) not null default false, \`frame_rate\` int not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`sprite\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`item\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`description\` varchar(255) null, \`item_type\` enum('WEAPON', 'HELMET', 'CHEST', 'LEGS', 'BOOTS', 'GLOVES', 'RING', 'NECKLACE') not null, \`stackable\` tinyint(1) not null default false, \`rarity\` enum('COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY') not null default 'COMMON', \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`item\` add index \`item_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_type\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` enum('MALE', 'FEMALE') not null, \`race\` enum('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') not null, \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_type\` add index \`character_type_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`character_hair\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`gender\` varchar(255) not null default 'MALE', \`is_selectable\` tinyint(1) not null default false, \`sprite_id\` varchar(255) null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_hair\` add index \`character_hair_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`sprite_action\` (\`id\` varchar(255) not null, \`sprite_id\` varchar(255) not null, \`action\` varchar(255) not null, \`sprites\` json null, \`origin_x\` int not null default 0, \`origin_y\` int not null default 0, \`is_animated\` tinyint(1) not null default false, \`is_looping\` tinyint(1) not null default false, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`frame_rate\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`sprite_action\` add index \`sprite_action_sprite_id_index\`(\`sprite_id\`);`);
this.addSql(`create table \`tile\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`user\` (\`id\` varchar(255) not null, \`username\` varchar(255) not null, \`email\` varchar(255) not null, \`password\` varchar(255) not null, \`online\` tinyint(1) not null default false, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`user\` add unique \`user_username_unique\`(\`username\`);`);
this.addSql(`alter table \`user\` add unique \`user_email_unique\`(\`email\`);`);
this.addSql(`create table \`password_reset_token\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`token\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`password_reset_token\` add index \`password_reset_token_user_id_index\`(\`user_id\`);`);
this.addSql(`alter table \`password_reset_token\` add unique \`password_reset_token_token_unique\`(\`token\`);`);
this.addSql(`create table \`world\` (\`date\` datetime not null, \`is_rain_enabled\` tinyint(1) not null default false, \`rain_percentage\` int not null default 0, \`is_fog_enabled\` tinyint(1) not null default false, \`fog_density\` int not null default 0, primary key (\`date\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`zone\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`character\` (\`id\` varchar(255) not null, \`user_id\` varchar(255) not null, \`name\` varchar(255) not null, \`online\` tinyint(1) not null default false, \`role\` varchar(255) not null default 'player', \`zone_id\` varchar(255) not null, \`position_x\` int not null default 0, \`position_y\` int not null default 0, \`rotation\` int not null default 0, \`character_type_id\` varchar(255) null, \`character_hair_id\` varchar(255) null, \`alignment\` int not null default 50, \`hitpoints\` int not null default 100, \`mana\` int not null default 100, \`level\` int not null default 1, \`experience\` int not null default 0, \`strength\` int not null default 10, \`dexterity\` int not null default 10, \`intelligence\` int not null default 10, \`wisdom\` int not null default 10, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character\` add index \`character_user_id_index\`(\`user_id\`);`);
this.addSql(`alter table \`character\` add unique \`character_name_unique\`(\`name\`);`);
this.addSql(`alter table \`character\` add index \`character_zone_id_index\`(\`zone_id\`);`);
this.addSql(`alter table \`character\` add index \`character_character_type_id_index\`(\`character_type_id\`);`);
this.addSql(`alter table \`character\` add index \`character_character_hair_id_index\`(\`character_hair_id\`);`);
this.addSql(`create table \`chat\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`message\` varchar(255) not null, \`created_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`chat\` add index \`chat_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`chat\` add index \`chat_zone_id_index\`(\`zone_id\`);`);
this.addSql(`create table \`character_item\` (\`id\` varchar(255) not null, \`character_id\` varchar(255) not null, \`item_id\` varchar(255) not null, \`quantity\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_item\` add index \`character_item_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`character_item\` add index \`character_item_item_id_index\`(\`item_id\`);`);
this.addSql(`create table \`character_equipment\` (\`id\` varchar(255) not null, \`slot\` enum('HEAD', 'BODY', 'ARMS', 'LEGS', 'NECK', 'RING') not null, \`character_id\` varchar(255) not null, \`character_item_id\` varchar(255) not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_id_index\`(\`character_id\`);`);
this.addSql(`alter table \`character_equipment\` add index \`character_equipment_character_item_id_index\`(\`character_item_id\`);`);
this.addSql(`create table \`zone_effect\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`effect\` varchar(255) not null, \`strength\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`zone_effect\` add index \`zone_effect_zone_id_index\`(\`zone_id\`);`);
this.addSql(`create table \`zone_event_tile\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`type\` enum('BLOCK', 'TELEPORT', 'NPC', 'ITEM') not null, \`position_x\` int not null, \`position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`zone_event_tile\` add index \`zone_event_tile_zone_id_index\`(\`zone_id\`);`);
this.addSql(`create table \`zone_event_tile_teleport\` (\`id\` varchar(255) not null, \`zone_event_tile_id\` varchar(255) not null, \`to_zone_id\` varchar(255) not null, \`to_rotation\` int not null, \`to_position_x\` int not null, \`to_position_y\` int not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`zone_event_tile_teleport\` add unique \`zone_event_tile_teleport_zone_event_tile_id_unique\`(\`zone_event_tile_id\`);`);
this.addSql(`alter table \`zone_event_tile_teleport\` add index \`zone_event_tile_teleport_to_zone_id_index\`(\`to_zone_id\`);`);
this.addSql(`create table \`zone_object\` (\`id\` varchar(255) not null, \`zone_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`depth\` int not null default 0, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`zone_object\` add index \`zone_object_zone_id_index\`(\`zone_id\`);`);
this.addSql(`alter table \`zone_object\` add index \`zone_object_map_object_id_index\`(\`map_object_id\`);`);
this.addSql(`alter table \`item\` add constraint \`item_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character_type\` add constraint \`character_type_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character_hair\` add constraint \`character_hair_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`sprite_action\` add constraint \`sprite_action_sprite_id_foreign\` foreign key (\`sprite_id\`) references \`sprite\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`password_reset_token\` add constraint \`password_reset_token_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_user_id_foreign\` foreign key (\`user_id\`) references \`user\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade;`);
this.addSql(`alter table \`character\` add constraint \`character_character_type_id_foreign\` foreign key (\`character_type_id\`) references \`character_type\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`character\` add constraint \`character_character_hair_id_foreign\` foreign key (\`character_hair_id\`) references \`character_hair\` (\`id\`) on update cascade on delete set null;`);
this.addSql(`alter table \`chat\` add constraint \`chat_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`chat\` add constraint \`chat_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_item\` add constraint \`character_item_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_item\` add constraint \`character_item_item_id_foreign\` foreign key (\`item_id\`) references \`item\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_equipment\` add constraint \`character_equipment_character_id_foreign\` foreign key (\`character_id\`) references \`character\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`character_equipment\` add constraint \`character_equipment_character_item_id_foreign\` foreign key (\`character_item_id\`) references \`character_item\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`zone_effect\` add constraint \`zone_effect_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`zone_event_tile\` add constraint \`zone_event_tile_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`zone_event_tile_teleport\` add constraint \`zone_event_tile_teleport_zone_event_tile_id_foreign\` foreign key (\`zone_event_tile_id\`) references \`zone_event_tile\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`zone_event_tile_teleport\` add constraint \`zone_event_tile_teleport_to_zone_id_foreign\` foreign key (\`to_zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`zone_object\` add constraint \`zone_object_zone_id_foreign\` foreign key (\`zone_id\`) references \`zone\` (\`id\`) on update cascade on delete cascade;`);
this.addSql(`alter table \`zone_object\` add constraint \`zone_object_map_object_id_foreign\` foreign key (\`map_object_id\`) references \`map_object\` (\`id\`) on update cascade on delete cascade;`);
}
}

27
mikro-orm.config.ts Normal file
View File

@ -0,0 +1,27 @@
// import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb'
import { Migrator } from '@mikro-orm/migrations'
import { defineConfig, MySqlDriver } from '@mikro-orm/mysql'
import { TsMorphMetadataProvider } from '@mikro-orm/reflection'
import serverConfig from './src/application/config'
export default defineConfig({
extensions: [Migrator],
metadataProvider: TsMorphMetadataProvider,
entities: ['./src/entities/**/*.js'],
entitiesTs: ['./src/entities/**/*.ts'],
driver: MySqlDriver,
host: serverConfig.DB_HOST,
port: serverConfig.DB_PORT,
user: serverConfig.DB_USER,
password: serverConfig.DB_PASS,
dbName: serverConfig.DB_NAME,
// debug: serverConfig.ENV !== 'production',
driverOptions: {
allowPublicKeyRetrieval: true
},
migrations: {
path: './migrations',
pathTs: './migrations',
}
})

5433
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,33 @@
{
"useTsNode": true,
"alwaysAllowTs": true,
"scripts": {
"start": "npx prisma migrate deploy && node dist/server.js",
"dev": "nodemon --exec ts-node src/server.ts",
"start": "node dist/server.js",
"dev": "nodemon --exec tsx src/server.ts",
"build": "tsc",
"format": "prettier --write src/"
"format": "prettier --write src/",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@prisma/client": "^5.17.0",
"@mikro-orm/core": "^6.4.2",
"@mikro-orm/mariadb": "^6.4.2",
"@mikro-orm/migrations": "^6.4.2",
"@mikro-orm/mysql": "^6.4.2",
"@mikro-orm/reflection": "^6.4.2",
"@prisma/client": "^6.1.0",
"@types/blessed": "^0.1.25",
"@types/ioredis": "^4.28.10",
"bcryptjs": "^2.4.3",
"blessed": "^0.1.81",
"bullmq": "^5.13.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
"pino": "^9.3.2",
"prisma": "^5.17.0",
"sharp": "^0.33.4",
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
@ -24,11 +35,20 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@mikro-orm/cli": "^6.4.2",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.14.11",
"@types/nodemailer": "^6.4.16",
"@typescript-eslint/eslint-plugin": "^8.18.2",
"@typescript-eslint/parser": "^8.18.2",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"nodemon": "^3.1.4",
"prettier": "^3.3.3"
"prettier": "^3.3.3",
"prisma": "^6.1.0",
"tsx": "^4.19.2"
}
}

View File

@ -1,224 +0,0 @@
-- CreateTable
CREATE TABLE `Chat` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`characterId` INTEGER NOT NULL,
`zoneId` INTEGER NOT NULL,
`message` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Sprite` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `SpriteAction` (
`id` VARCHAR(191) NOT NULL,
`spriteId` VARCHAR(191) NOT NULL,
`action` VARCHAR(191) NOT NULL,
`sprites` JSON NULL,
`originX` DECIMAL(65, 30) NOT NULL DEFAULT 0,
`originY` DECIMAL(65, 30) NOT NULL DEFAULT 0,
`isAnimated` BOOLEAN NOT NULL DEFAULT false,
`isLooping` BOOLEAN NOT NULL DEFAULT false,
`frameWidth` INTEGER NOT NULL DEFAULT 0,
`frameHeight` INTEGER NOT NULL DEFAULT 0,
`frameSpeed` INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`username` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL,
`online` BOOLEAN NOT NULL DEFAULT false,
UNIQUE INDEX `User_username_key`(`username`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CharacterType` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`gender` ENUM('MALE', 'FEMALE') NOT NULL,
`race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL,
`spriteId` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Character` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`name` VARCHAR(191) NOT NULL,
`online` BOOLEAN NOT NULL DEFAULT false,
`hitpoints` INTEGER NOT NULL DEFAULT 100,
`mana` INTEGER NOT NULL DEFAULT 100,
`level` INTEGER NOT NULL DEFAULT 1,
`experience` INTEGER NOT NULL DEFAULT 0,
`alignment` INTEGER NOT NULL DEFAULT 50,
`role` VARCHAR(191) NOT NULL DEFAULT 'player',
`positionX` INTEGER NOT NULL DEFAULT 0,
`positionY` INTEGER NOT NULL DEFAULT 0,
`rotation` INTEGER NOT NULL DEFAULT 0,
`zoneId` INTEGER NOT NULL DEFAULT 1,
`characterTypeId` INTEGER NULL,
UNIQUE INDEX `Character_name_key`(`name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CharacterItem` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`characterId` INTEGER NOT NULL,
`itemId` VARCHAR(191) NOT NULL,
`quantity` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Tile` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`tags` JSON NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Object` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`tags` JSON NULL,
`originX` DECIMAL(65, 30) NOT NULL DEFAULT 0,
`originY` DECIMAL(65, 30) NOT NULL DEFAULT 0,
`isAnimated` BOOLEAN NOT NULL DEFAULT false,
`frameSpeed` INTEGER NOT NULL DEFAULT 0,
`frameWidth` INTEGER NOT NULL DEFAULT 0,
`frameHeight` INTEGER NOT NULL DEFAULT 0,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Item` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NULL,
`stackable` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Zone` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`width` INTEGER NOT NULL DEFAULT 10,
`height` INTEGER NOT NULL DEFAULT 10,
`tiles` JSON NULL,
`pvp` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ZoneObject` (
`id` VARCHAR(191) NOT NULL,
`zoneId` INTEGER NOT NULL,
`objectId` VARCHAR(191) NOT NULL,
`depth` INTEGER NOT NULL DEFAULT 0,
`positionX` INTEGER NOT NULL DEFAULT 0,
`positionY` INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ZoneEventTile` (
`id` VARCHAR(191) NOT NULL,
`zoneId` INTEGER NOT NULL,
`type` ENUM('BLOCK', 'TELEPORT', 'NPC', 'ITEM') NOT NULL,
`positionX` INTEGER NOT NULL,
`positionY` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ZoneEventTileTeleport` (
`id` VARCHAR(191) NOT NULL,
`zoneEventTileId` VARCHAR(191) NOT NULL,
`toZoneId` INTEGER NOT NULL,
`toPositionX` INTEGER NOT NULL,
`toPositionY` INTEGER NOT NULL,
UNIQUE INDEX `ZoneEventTileTeleport_zoneEventTileId_key`(`zoneEventTileId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Chat` ADD CONSTRAINT `Chat_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Character` ADD CONSTRAINT `Character_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Character` ADD CONSTRAINT `Character_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Character` ADD CONSTRAINT `Character_characterTypeId_fkey` FOREIGN KEY (`characterTypeId`) REFERENCES `CharacterType`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FOREIGN KEY (`characterId`) REFERENCES `Character`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_objectId_fkey` FOREIGN KEY (`objectId`) REFERENCES `Object`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ZoneEventTile` ADD CONSTRAINT `ZoneEventTile_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ZoneEventTileTeleport` ADD CONSTRAINT `ZoneEventTileTeleport_zoneEventTileId_fkey` FOREIGN KEY (`zoneEventTileId`) REFERENCES `ZoneEventTile`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ZoneEventTileTeleport` ADD CONSTRAINT `ZoneEventTileTeleport_toZoneId_fkey` FOREIGN KEY (`toZoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"

View File

@ -1,31 +0,0 @@
// CHEAT SHEET
// 1. Create a new Prisma project
// npx prisma init
// 2. Create a new database schema
// npx prisma db push
// 3. Generate Prisma Client and type-safe models based on schema
// npx prisma generate
// 4. Create a new migration
// npx prisma migrate dev --name [migration-name]
// 5. Apply the migration
// npx prisma migrate deploy
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Chat {
id Int @id @default(autoincrement())
characterId Int
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
zoneId Int
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
message String
createdAt DateTime
}

View File

@ -1,23 +0,0 @@
model Sprite {
id String @id @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
spriteActions SpriteAction[]
characterTypes CharacterType[]
}
model SpriteAction {
id String @id @default(uuid())
spriteId String
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
action String
sprites Json?
originX Decimal @default(0)
originY Decimal @default(0)
isAnimated Boolean @default(false)
isLooping Boolean @default(false)
frameWidth Int @default(0)
frameHeight Int @default(0)
frameSpeed Int @default(0)
}

View File

@ -1,64 +0,0 @@
model User {
id Int @id @default(autoincrement())
username String @unique
password String
online Boolean @default(false)
characters Character[]
}
enum CharacterGender {
MALE
FEMALE
}
enum CharacterRace {
HUMAN
ELF
DWARF
ORC
GOBLIN
}
model CharacterType {
id Int @id @default(autoincrement())
name String
gender CharacterGender
race CharacterRace
characters Character[]
spriteId String
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Character {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String @unique
online Boolean @default(false)
hitpoints Int @default(100)
mana Int @default(100)
level Int @default(1)
experience Int @default(0)
alignment Int @default(50)
role String @default("player")
positionX Int @default(0)
positionY Int @default(0)
rotation Int @default(0)
zoneId Int @default(1)
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
characterTypeId Int?
characterType CharacterType? @relation(fields: [characterTypeId], references: [id], onDelete: Cascade)
chats Chat[]
items CharacterItem[]
}
model CharacterItem {
id Int @id @default(autoincrement())
characterId Int
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
itemId String
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
quantity Int
}

View File

@ -1,86 +0,0 @@
model Tile {
id String @id @default(uuid())
name String
tags Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Object {
id String @id @default(uuid())
name String
tags Json?
originX Decimal @default(0)
originY Decimal @default(0)
isAnimated Boolean @default(false)
frameSpeed Int @default(0)
frameWidth Int @default(0)
frameHeight Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ZoneObject ZoneObject[]
}
model Item {
id String @id @default(uuid())
name String
description String?
stackable Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
characters CharacterItem[]
}
model Zone {
id Int @id @default(autoincrement())
name String
width Int @default(10)
height Int @default(10)
tiles Json?
pvp Boolean @default(false)
zoneEventTiles ZoneEventTile[]
zoneEventTileTeleports ZoneEventTileTeleport[]
zoneObjects ZoneObject[]
characters Character[]
chats Chat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ZoneObject {
id String @id @default(uuid())
zoneId Int
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
objectId String
object Object @relation(fields: [objectId], references: [id], onDelete: Cascade)
depth Int @default(0)
positionX Int @default(0)
positionY Int @default(0)
}
enum ZoneEventTileType {
BLOCK
TELEPORT
NPC
ITEM
}
model ZoneEventTile {
id String @id @default(uuid())
zoneId Int
zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade)
type ZoneEventTileType
positionX Int
positionY Int
teleport ZoneEventTileTeleport?
}
model ZoneEventTileTeleport {
id String @id @default(uuid())
zoneEventTileId String @unique
zoneEventTile ZoneEventTile @relation(fields: [zoneEventTileId], references: [id], onDelete: Cascade)
toZoneId Int
toZone Zone @relation(fields: [toZoneId], references: [id], onDelete: Cascade)
toPositionX Int
toPositionY Int
}

3
public/.gitignore vendored
View File

@ -1,2 +1,3 @@
**
!.gitignore
**
!assets.zip

BIN
public/assets.zip Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseCommand {
protected readonly logger = Logger.type(LoggerType.COMMAND)
constructor(readonly io: Server) {}
}

View File

@ -0,0 +1,22 @@
import { Request, Response } from 'express'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseController {
protected readonly logger = Logger.type(LoggerType.HTTP)
protected sendSuccess(res: Response, data?: any, message?: string, status: number = 200) {
return res.status(status).json({
success: true,
message,
data
})
}
protected sendError(res: Response, message: string, status: number = 400) {
return res.status(status).json({
success: false,
message
})
}
}

View File

@ -0,0 +1,44 @@
import { EntityManager } from '@mikro-orm/core'
import Database from '#application/database'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseEntity {
protected readonly logger = Logger.type(LoggerType.ENTITY)
private getEntityManager(): EntityManager {
return Database.getEntityManager()
}
async save(): Promise<this> {
return this.execute('persist', 'save entity')
}
async update(): Promise<this> {
return this.execute('merge', 'update entity')
}
async delete(): Promise<this> {
return this.execute('remove', 'remove entity')
}
private async execute(method: 'persist' | 'merge' | 'remove', actionDescription: string): Promise<this> {
try {
const em = this.getEntityManager()
await em.begin()
try {
em[method](this)
await em.flush()
await em.commit()
return this
} catch (error) {
await em.rollback()
throw error
}
} catch (error) {
this.logger.error(`Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
}

View File

@ -0,0 +1,24 @@
import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger'
import { TSocket } from '#application/types'
export abstract class BaseEvent {
protected readonly logger = Logger.type(LoggerType.GAME)
constructor(
readonly io: Server,
readonly socket: TSocket
) {}
protected emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message })
this.logger.error('character:connect error', `Player ${this.socket.userId}: ${message}`)
}
protected handleError(context: string, error: unknown): void {
const errorMessage = error instanceof Error ? error.message : String(error)
this.emitError(`${context}: ${errorMessage}`)
this.logger.error('character:connect error', errorMessage)
}
}

View File

@ -0,0 +1,13 @@
import { EntityManager } from '@mikro-orm/core'
import Database from '../database'
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseRepository {
protected readonly logger = Logger.type(LoggerType.REPOSITORY)
protected get em(): EntityManager {
return Database.getEntityManager()
}
}

View File

@ -0,0 +1,5 @@
import Logger, { LoggerType } from '#application/logger'
export abstract class BaseService {
protected readonly logger = Logger.type(LoggerType.GAME)
}

36
src/application/config.ts Normal file
View File

@ -0,0 +1,36 @@
import dotenv from 'dotenv'
dotenv.config()
class config {
// Server configuration
static ENV: string = process.env.ENV || 'development'
static HOST: string = process.env.HOST || '0.0.0.0'
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://noxious.gg'
// Database configuration
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
static DB_HOST: string = process.env.DB_HOST || 'localhost'
static DB_USER: string = process.env.DB_USER || 'root'
static DB_PASS: string = process.env.DB_PASS || ''
static DB_PORT: number = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306
static DB_NAME: string = process.env.DB_NAME || 'game'
// Game configuration
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
// Default character create values
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
// Email configuration
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@noxious.gg'
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
}
export default config

View File

@ -0,0 +1,38 @@
import { EntityManager } from '@mikro-orm/core'
import { MikroORM } from '@mikro-orm/mysql'
import Logger, { LoggerType } from './logger'
import config from '../../mikro-orm.config'
class Database {
private static orm: MikroORM
private static em: EntityManager
private static logger = Logger.type(LoggerType.APP)
public static async initialize(): Promise<void> {
try {
Database.orm = await MikroORM.init(config)
Database.em = Database.orm.em.fork()
this.logger.info('Database connection initialized')
} catch (error) {
this.logger.error(`MikroORM connection failed: ${error}`)
throw error
}
}
public static getORM(): MikroORM {
if (!Database.orm) {
throw new Error('Database not initialized. Call Database.initialize() first.')
}
return Database.orm
}
public static getEntityManager(): EntityManager {
if (!Database.em) {
throw new Error('Database not initialized. Call Database.initialize() first.')
}
return Database.em
}
}
export default Database

63
src/application/enums.ts Normal file
View File

@ -0,0 +1,63 @@
export enum SocketEvent {
CHARACTER_CONNECT = 1,
CHARACTER_MOVE = 2,
CHARACTER_MOVE_ERROR = 3,
CHARACTER_TELEPORT = 4,
ZONE_CHARACTER_LEAVE = 5,
ZONE_CHARACTER_JOIN = 6,
ZONE_CHARACTER_LIST = 7,
ZONE_CHARACTER_DELETE = 8,
ZONE_CHARACTER_CREATE = 9,
ZONE_CHARACTER_UPDATE = 10,
ZONE_CHARACTER_HAIR_UPDATE = 11,
ZONE_CHARACTER_HAIR_LIST = 12,
ZONE_CHARACTER_TELEPORT = 13
}
export enum ItemType {
WEAPON = 'WEAPON',
HELMET = 'HELMET',
CHEST = 'CHEST',
LEGS = 'LEGS',
BOOTS = 'BOOTS',
GLOVES = 'GLOVES',
RING = 'RING',
NECKLACE = 'NECKLACE'
}
export enum ItemRarity {
COMMON = 'COMMON',
UNCOMMON = 'UNCOMMON',
RARE = 'RARE',
EPIC = 'EPIC',
LEGENDARY = 'LEGENDARY'
}
export enum CharacterGender {
MALE = 'MALE',
FEMALE = 'FEMALE'
}
export enum CharacterRace {
HUMAN = 'HUMAN',
ELF = 'ELF',
DWARF = 'DWARF',
ORC = 'ORC',
GOBLIN = 'GOBLIN'
}
export enum CharacterEquipmentSlotType {
HEAD = 'HEAD',
BODY = 'BODY',
ARMS = 'ARMS',
LEGS = 'LEGS',
NECK = 'NECK',
RING = 'RING'
}
export enum ZoneEventTileType {
BLOCK = 'BLOCK',
TELEPORT = 'TELEPORT',
NPC = 'NPC',
ITEM = 'ITEM'
}

52
src/application/logger.ts Normal file
View File

@ -0,0 +1,52 @@
import pino from 'pino'
export enum LoggerType {
HTTP = 'http',
GAME = 'game',
GAME_MASTER = 'gameMaster',
APP = 'app',
QUEUE = 'queue',
COMMAND = 'command',
REPOSITORY = 'repository',
ENTITY = 'entity',
CONSOLE = 'console'
}
class Logger {
private instances: Map<LoggerType, ReturnType<typeof pino>> = new Map()
private getLogger(type: LoggerType): ReturnType<typeof pino> {
if (!this.instances.has(type)) {
this.instances.set(
type,
pino({
level: process.env.LOG_LEVEL || 'debug',
transport: {
target: 'pino/file',
options: {
destination: `./logs/${type}.log`,
mkdir: true
}
},
formatters: {
level: (label) => ({ level: label.toUpperCase() })
},
timestamp: pino.stdTimeFunctions.isoTime,
base: null
})
)
}
return this.instances.get(type)!
}
type(type: LoggerType) {
return {
info: (message: string, ...args: any[]) => this.getLogger(type).info(message, ...args),
error: (message: string, ...args: any[]) => this.getLogger(type).error(message, ...args),
warn: (message: string, ...args: any[]) => this.getLogger(type).warn(message, ...args),
debug: (message: string, ...args: any[]) => this.getLogger(type).debug(message, ...args)
}
}
}
export default new Logger()

View File

@ -0,0 +1,71 @@
import fs from 'fs'
import path from 'path'
import config from '#application/config'
class Storage {
private readonly baseDir: string
private readonly rootDir: string
constructor() {
this.rootDir = process.cwd()
this.baseDir = config.ENV === 'development' ? 'src' : 'dist'
}
/**
* Gets path relative to project root
*/
public getRootPath(folder: string, ...additionalSegments: string[]): string {
return path.join(this.rootDir, folder, ...additionalSegments)
}
/**
* Gets path relative to app directory (src/dist)
*/
public getAppPath(folder: string, ...additionalSegments: string[]): string {
return path.join(this.rootDir, this.baseDir, folder, ...additionalSegments)
}
/**
* Gets path relative to public directory
*/
public getPublicPath(folder: string, ...additionalSegments: string[]): string {
return path.join(this.rootDir, 'public', folder, ...additionalSegments)
}
/**
* Checks if a path exists
* @throws Error if path is empty or invalid
*/
public doesPathExist(pathToCheck: string): boolean {
if (!pathToCheck) {
throw new Error('Path cannot be empty')
}
try {
fs.accessSync(pathToCheck, fs.constants.F_OK)
return true
} catch (e) {
return false
}
}
/**
* Creates a directory and any necessary parent directories
* @throws Error if directory creation fails
*/
public createDir(dirPath: string): void {
if (!dirPath) {
throw new Error('Directory path cannot be empty')
}
try {
fs.mkdirSync(dirPath, { recursive: true })
} catch (error) {
const typedError = error as Error
throw new Error(`Failed to create directory: ${typedError.message}`)
}
}
}
export default new Storage()

63
src/application/types.ts Normal file
View File

@ -0,0 +1,63 @@
import { Server, Socket } from 'socket.io'
import { Character } from '#entities/character'
import { ZoneEventTile } from '#entities/zoneEventTile'
import { ZoneEventTileTeleport } from '#entities/zoneEventTileTeleport'
export type UUID = `${string}-${string}-${string}-${string}-${string}`
export type TSocket = Socket & {
userId?: UUID
characterId?: UUID
handshake?: {
query?: {
token?: any
}
}
request?: {
headers?: {
cookie?: any
}
}
}
export type ExtendedCharacter = Character & {
isMoving?: boolean
resetMovement?: boolean
}
export type ZoneEventTileWithTeleport = ZoneEventTile & {
teleport: ZoneEventTileTeleport
}
export type AssetData = {
key: string
data: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
originX?: number
originY?: number
isAnimated?: boolean
frameRate?: number
frameWidth?: number
frameHeight?: number
frameCount?: number
}
export type WorldSettings = {
date: Date
isRainEnabled: boolean
isFogEnabled: boolean
fogDensity: number
}
export interface Command {
new (io: Server): {
execute(args: string[]): Promise<void>
}
}
// export type TCharacter = Socket & {
// user?: User
// character?: Character
// }

View File

@ -0,0 +1,3 @@
export function unduplicateArray(array: any[]) {
return [...new Set(array.flat())]
}

View File

@ -20,6 +20,29 @@ export const registerAccountSchema = z.object({
.min(3, { message: 'Name must be at least 3 characters long' })
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
email: z
.string()
.min(3, { message: 'Email must be at least 3 characters long' })
.max(255, { message: 'Email must be at most 255 characters long' })
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters long'
})
.max(255)
})
export const resetPasswordSchema = z.object({
email: z
.string()
.min(3, { message: 'Email must be at least 3 characters long' })
.max(255, { message: 'Email must be at most 255 characters long' })
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
})
export const newPasswordSchema = z.object({
urlToken: z.string().min(10, { message: 'Invalid request' }).max(255, { message: 'Invalid request' }),
password: z
.string()
.min(8, {

View File

@ -1,9 +1,13 @@
import { Server } from 'socket.io'
import { BaseCommand } from '#application/base/baseCommand'
type CommandInput = string[]
export default function (input: CommandInput, io: Server) {
const message: string = input.join(' ') ?? null
if (!message) return console.log('message is required')
io.emit('notification', { message: message })
export default class AlertCommand extends BaseCommand {
public execute(input: CommandInput): void {
const message: string = input.join(' ') ?? null
if (!message) return console.log('message is required')
this.io.emit('notification', { message: message })
}
}

255
src/commands/init.ts Normal file
View File

@ -0,0 +1,255 @@
import fs from 'fs'
import sharp from 'sharp'
import { BaseCommand } from '#application/base/baseCommand'
import { CharacterGender, CharacterRace } from '#application/enums'
import Storage from '#application/storage'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { CharacterHair } from '#entities/characterHair'
import { CharacterType } from '#entities/characterType'
import { MapObject } from '#entities/mapObject'
import { Sprite } from '#entities/sprite'
import { SpriteAction } from '#entities/spriteAction'
import { Tile } from '#entities/tile'
import { User } from '#entities/user'
import { Zone } from '#entities/zone'
import { ZoneEffect } from '#entities/zoneEffect'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import ZoneRepository from '#repositories/zoneRepository'
// @TODO : Replace this with seeding
// https://mikro-orm.io/docs/seeding
export default class InitCommand extends BaseCommand {
public async execute(): Promise<void> {
// Assets
await this.importTiles()
await this.importObjects()
await this.createCharacterType()
await this.createCharacterHair()
// await this.createCharacterEquipment()
// Zone
await this.createZone()
// User
await this.createUser()
// Stop process
process.exit(0)
}
private async importTiles(): Promise<void> {
for (const tile of fs.readdirSync(Storage.getPublicPath('tiles'))) {
const newTile = new Tile()
newTile.setId(tile.split('.')[0] as UUID).setName('New tile')
await newTile.save()
}
}
private async importObjects(): Promise<void> {
for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) {
const newMapObject = new MapObject()
newMapObject
.setId(object.split('.')[0] as UUID)
.setName('New object')
.setFrameWidth(
(await sharp(Storage.getPublicPath('objects', object))
.metadata()
.then((metadata) => metadata.height)) ?? 0
)
.setFrameHeight(
(await sharp(Storage.getPublicPath('objects', object))
.metadata()
.then((metadata) => metadata.width)) ?? 0
)
await newMapObject.save()
}
}
private async createCharacterType(): Promise<void> {
const characterSprite = new Sprite()
characterSprite.setId('023d1e9d-f57f-4faa-8412-86c07107cf85').setName('Character')
await characterSprite.save()
const idleRightDownAction = new SpriteAction()
await idleRightDownAction
.setAction('idle_right_down')
.setSprites([
'data:image/png;base64,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'
])
.setOriginX(0)
.setOriginY(0)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64)
.setFrameHeight(94)
.setFrameRate(0)
.setSprite(characterSprite)
.save()
const idleLeftUpAction = new SpriteAction()
await idleLeftUpAction
.setAction('idle_left_up')
.setSprites([
'data:image/png;base64,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'
])
.setOriginX(0)
.setOriginY(0)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64)
.setFrameHeight(94)
.setFrameRate(0)
.setSprite(characterSprite)
.save()
const walkRightDownAction = new SpriteAction()
await walkRightDownAction
.setAction('walk_right_down')
.setSprites([
'data:image/png;base64,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',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAABbCAYAAACGeS4EAAAGr0lEQVR4nK1ZTWgbRxT+tDaKsSNUJEeKwbCBZNFFogoUjBvIxVV6CT4kp1K3+BAaEwqGBAI51ASXHlpwoNCf5CbSXFJ68tHGF1+CL42MDEUoAQkMiYQsYhQLxxC7h80bvZ2f3ZWdDxak3dn55r15f/M2MnXvEfyQWF480t1vTy9EfF9kGAyafDZ/DhenLivP55fc52HItCSJ5cWj2fw5JFJp2LkMhpwshkdHPGMePcxiv7qF+aXFoyAiy0Rwceqyh2Ag6Xiu4dERDDlZ/HrnW6NKjSQAkEilAQBDTtb4Yj9EHhKSws5l/BamwG8xCokJ3dYe3u9Ufcf4SWO0Lh3RMKrKvTAITdLPpDIUdU3O3UK9XAEAHO52Qk80v/Q4PMl+dQuZa9fxfG0dB82659nNubu4OXdXmeTm3F2sZy+EJyEpJudu4fnaujLR082aQvR0s2Yk0JK0mw3sV7cAuP5CKqOJxr7+InBSX5L29EKkWKoJaexcBgfNOg53O3jz7G8xjv+m/8+//NxIorUukqZergiiKGz89/13nnE8nu37uJGqLiZNu9kQ98kIhkdHxEUIMm2tx7enFyIPVjYAwLP5h7sddFt7ynW428H80mNj2PcNK8VSzUNE+yNfsqnL8PX42fw5QUTQJzCzFIEkAHD7ygTazYYgk0mB4OwY0eV4OTOaUC9XQHsXWhJTXqfs2G3tCUcdcrLIOFnchmvyxWVzzheS8LQbTdliAN9UbtaUPUlSelYs1RSiQR3B6fNnxap51ss4WSEJOSo9twGgDAA1RV3ChGllp8+fxUDSESoBgFgmL64zV2cQTdmevaL7pGo5Q1o8rw85WUEQFuT5lJ5psRxi46MpW6mtDnc7sOIx3/xOadkvtFjEbsVjQgp5Uh5C3r58rSUi6Eze0j3gL8khBHAtjtLBfnULh7sdj2nLGAS8qiIpKCbpzJnMtb3Wi9LFUg1//fO7Vm1KWKFB1bUVkUsIOj8BIO4NJB2gVfIn4VLIKwXc4OikrnjuHTTryrhASTh47IqmbFjxGACIEAMAUdied2hvfEl4fpAJqLoHgFjSlbzL3jWZugVASTr1ckWEdC6B7Kg6xyWrU0g4Dpp1T24nyI6qu29ySMvE7of3O1WhGj6xbj+APgtuIhATfzBX15xrAIAHKxtKqA91PgGOX9EDgMXLH8ANC7pIehIyIUm3tWcM8xSzuIma9O9L0i/6MRZBErQyqh4JmWvXxf3QJHxlpjJIVz2GgSAhB+TOVS9XAktQDl43c/j6SbvZAMqAnXP/89xC4BGC+4tC0p5eiBSXF4+mdqoYSDpITAKz+XUUSzXspE8Bmxu4M/ZOTCTXWkuvTgHpjHufuYNWkvazDZy56nhWVSgUAABLq6vuoFdvgE0+0SkUCgWs0nMNjOriq0qn05iZmTFO0mj0kbTq5QoSk67Dkf7Tadf7L1zoHaHHx8fF7+3tbV8ChYQwkHRgffCJRqOBS5cuaQn4/0KhgJknq0hq5vOeflkeMeUPHcbHxz0LMZLQgfTty9ciRj16+Av+ffIbXrx4IV7Y3t7WqkiWkMO48QNJBzEmOyeSJ75x4waSjQqS0J9PFJKDZh3deAyxJNCplAC4+0IGwEnv378PAK4Pjanny0BJAODZwz8AwOgDd8beCeeslyuYzQPFZbXxqZDUyxU4KduTO3769BMAPcMolmrgZ0r3vAJjkadt4MhBkcKIKWNa8RiiKdv83EMgNXB0RHQmlPV/+vxZ2LmM/qSlY243G54ERd6vq8eAnk+ZuqrGBo6cKXVhXiYdHh3xP87JoJ7inz//AMA1bd3xOQwUElM3ot8qk+PY1QrHsfpdOsj65xZG+9dt7WktzPeDANA7Vpj2g9T49uVrY/Wi/SAgTxC2YjE5ZahqxeQfNMZGRpzEupox6kcatopEKm3s/Mggh9Qlu14Dh6nKzmVCfUPhYYgsrNvaUyQ3qosiq6m9JGO/umXsDXs3PqAdeFxoJdHFqTCgAJkoV8Cld7tEkukCvSaN7gzoB9+NPw744nTfIhUSvh/9HBfCwNKpCkBfqrJzGd9i8KNE4SBoq5WPDUUS8nZTUSAjkUp7TF6XWxSSaMoW18RX3/S9al2413aJCNW1lb4ITBlykM6Ls3m3AnSjsLsvfiFeR2D6YOM5mLq3ap4BQSZMvuV3phfWdZxSJyxO7CdhgulHd0b6InH7yoQw/xOT8I224jFY8RgSkxP47N6Pws9OTEL9F94zBtzeGNULffcgZYj+y4f/PAWTC2i/zvWDoLDTnl6I/A+0buJfTRF5tQAAAABJRU5ErkJggg==',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAABcCAYAAAAPmrdOAAAHOklEQVR4nNVaTWgbRxT+/INi/ENANpINgTXYYi8StS81LjQX1+7Nh+RYU3xqTCgYYgiUUh986cmGXEpyE2mhpEcfbQRBEIKhIAUbipADEgSSXewlQrFxDJZ7kN9oZnZGO7uWC/1gsXa1mvfNe/Pe+2bXXXM/PUNYxLc3LlXXvcX1rrBj9UYxvDw1jum5u77vVzeb34chYkwgvr1xuTw1jngiCStjoy+VRv/IgHDPs6dpnJUPsLq5cWlKojuM8em5u4LxnuGUcPSPDKAvlcaTte+1YYpEAADiiSQAoC+V1t4ThUQgAZq9lbFNuQYSDUVAh9OjE1wcl9veY+KFUFmgItGPsu9aGFyLQBSDMoxCMLvyENX9EgCgUasbD766+bwzBM7KB7Dv3Uchl8e5WxW+e7DyGA9WHvt+82DlMfLpyc4QoNnPrjxEIZf3GXnxpuIj8eJNxWRoMwKe6+CsfACgWQ8oDGRk7LtvjA2GJuAtrndlixXmBStj49ytolGr4+Prv9h9/Gc6L3z7VSAB4ywgL1T3S4xEDBb++fEH4T6+P5y1LxMATEPAecFzHXadFmT/yAA7CKbpaVwJvcX1rq2dPQAQFmKjVsfp0YnvaNTqWN18HtiaQ5fibLEikKD1IB9yuuoQuhIuT40zEgS1OAmefSQCRAJoeUMmBJirokgEaMbTc3fZwiSlZDpzYwJ8OyVJxsPK2LDQ0gqPFmawxf0miIyWAC9AZYMAmCw7PTphVbIvlYadSuNJxmaeyW6314ddKllOKghoSTGdIuJrA4WBRyGXR7ZY0XrCR0BWv/HZGQDAp7cfAADdt4d8g/AVkpdidB0Atnb2lCSUdYA33jOcYobJeP/IAIbsKQzZU6z6kXH+el8qzTzyaGFGKc8EArwA1YlKkuME/rMK5BldCJUeiCUsZogXnlR2+WuyMK2XikqxGktYzSySvODLgngi6Ysz31gatTpOAeCo6DNyVj5AXyrN7qfs4McGKsI1bRrS7Gkwqu+xhKXVhdX9Eizp3HMdWLAxODEKy7WBq4ZG8IXAyti+PV+jVkc5twMAviZD54VcHp7roJDLs2NrZ4+lMa0VOQw+D8QSzTnQ7Bu1Ovb+/B3xRFIQI2SUIFdIqg3kARpbDgMjQBlA8SfXkzGq97JRK2Mz0rxXvFyTQLZYQTyRRHxWLY/a9oJGrS4Y5I3SjPjaQKRjEAl5roNPbz9gcGIUyIk2xDrAuZEWndxqZeNUlHqGU6wAqaoloK4ZAoHU3AJbgOdulc1eJsHPXB5UPic9ee5WcXFc9hUkgYAc/yDI2RJ0nfqJlsB1cHFcZhVQNQFqSoCYigIBmXm7NiobJzVcLxUBqNNSJVSVHmjU6gJjHaJuzXly3UCrBvQMp/Dp7Qecu1Whf9NC4jclYYjwG5v47IywEJkHeGltMnvqB3znk5uPDnzhYgRMHyq1gwlxOU0ZAT7/da6WQfqAYN+7z67rQB6jTOjlBWgUhHlkQxicGGVNiXmAXFPdL7VNv+p+yXjfJ4N6AtAq6b2AmBZB7vdcB9gHrEzzXO6EgFkY6Xe9PJt2OE7a2Hx/dfL+I/BmD2tjn0EToDFIBW2+vwUkbZAbvcX1ruz2xuX0XBWYGGXjKjWhrNsI8/Pzwvnm7q5AqIVbmJ+fxy59z6GQy2MmYeH2l1+rCVgZv27jkUy2wrW0tKS9z3H0IWh2xlGRwMVxGT3DKfSl0s1nAIo9HW98crL5DPDOnTvs2rt377RGgVYY4okkWwO+XjBkT/kaiQyVcdW5lgjXmBgBKih///qLcSGKAuoL9ByhGxBLqOc6xm1YxsuXL9nndmvAW1zvovG7yWijVg98/k+DHh4eAhBjTp8PDw/x6tUr7O7uYtgJ7g29cn7qZj/slEBZRek4OTkpGHYch6Xe2thnZA0iybKgkMtj7upZgA5rY58RTyTxsyK/5XtM11Ev0EqPWUOF88dSkyilUjm3w9ZO/IsrwbEP6AoaDyENXz/9zYgAbxxoFq+g1A0kQOlxHSxPjWNrZ89ImPgIEAld+ukIdt8eQixhRfZC6H0BLS5eEwxedTcrYzMv3AgB+eUF0FJEsYTFbb+DdUUkAoxIm8HDvmENHwLOCxSG06MTDE6MslDQE3WTch55b8h7gRemfBhMEC0EGi+QJ2gxduTteVsiXEbQmxIgnBciEaC9RLZY8T3C4b1wYwRUiLpXuBYB8oKuLpisg8gEVJsZuTqarIPQBHR7yTANiMe1/4FBJmFlAI97uPmfEOBf65AnbrQXqGJLxm+0F7R7lsAbtzK28m2qCh1bA/zMVVt2Ha5VB7LFiu+9Im/cpCN2rBJGxf+TgEmKmfaGjggSoFUJz90qzt2qcWWMJErp4MEXoTD1QPnyOgzi2xuXjxZmBBL012Sr35FFSOHgPWCKG82CG2nHJmj3svrGCfCGTdTxtQkEvcwIQkc94LmOsACDHnwDHUhDQtR/9/4Xd7FjcmoWqDQAAAAASUVORK5CYII=',
'data:image/png;base64,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'
])
.setOriginX(0)
.setOriginY(0)
.setIsAnimated(true)
.setIsLooping(false)
.setFrameWidth(64)
.setFrameHeight(94)
.setFrameRate(7)
.setSprite(characterSprite)
.save()
const walkLeftUpAction = new SpriteAction()
await walkLeftUpAction
.setAction('walk_left_up')
.setSprites([
'data:image/png;base64,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',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAABcCAYAAAALb2dzAAAGkUlEQVR4nL2bP2jcSBTGv92YjbFjNsiLdAcBGc6LGovYzZlgzo2JSxdJGw6Xx5VOdRy4WHBpw5XXLVwg1V2xpRc3hnC4smHTLBuDBQZbYi3O+A85g71XKG92JI2kGa1yX+OstNL89s17M+/NTEorv/yOPNJajYHour+2WcrzvrG8ABurizBtK3TvcG8fzS/3VYGUQLRWY7Cxuoj6yiqefPdN6N718TkWVpah6QZ8z0Wz1RiowEiDaK3G4Le3P2K8PoeJ2iQeTddD958AuAZg2oCJwFIqMGVZiI3VRVR0UwjBXladQkU3g2d0Q+bVaiAAUF9ZRbk6Jf1i07awPj+T6NTKIGSNNN1f9HDbvwleqADLS8pHTNtiDdz2bzCBXug+QYwi5fDNavjh8goA4HS6Su+U9hFqQFa+5+LFTz8XCyL76wj2cG9fCUIaJNpQ2r2D938wiM+9j9LvlvIR33OlYIDw+KHiJ9IWufMcqe/Q/PO59zH0A7KUaRF/bbPUbDUGmm7AtMP3KroZA3Q6XZj4ilHje27my+m+0+kqWUMaxF/bLDWPTmIw/ChK133PVYaQBuFh+EZFELyaRyfSeYlS+Pprm6Wd3YPM7zWPTpQglEGiSgtl1QytJJOz8lM5zcTRNJF0uLcPILCKClQqCAGsz8+wgcq0rVjYUjJE16JRQ1BpQIkgWqsxWJ+fAQAsrCyHGi1Xp/BweYU7z2GfJ2qTuD4+DwERFBA4cprfCAc0gggGsWEX8I3eAoAXXJ+oTQIAAyQLlatTqJP1OsD6fHIem+ismm7AevUa2otFBkF6NF1njfPXSLxFqt//gIpuwrSt1Dw2BkLWsF69Dn55/0Y4ovINi5Jpgrm/6IVgkvJYoUU03WAQNJXzv/L+ohd7JnrN6XTZs/cXPTYKJ1lFCMLPoE6ni53dg5hV+IZFYATj/32A2/4Npqz5UPemglC3jNfnWJfkmTfoOZqbHi6vmFWSuifuI1+6hfIJfmBSFU2Ud56D6+PzmIOnggBg40FS3MuUD/Rs8+gk1q0iP2Eg1C2mbUllYyq1DFklTYnjSFISlDTRJYHxViFLi/wk7KycydKcNAmGrouezbJKCCRpRs1SVjdFfaWimzE/KQPhCY6UldhErSJTCaZZeeisusEGHNUMXFXR1aYQCF/xJympj6WsIQhl3mGFUZNnNCXJhD4Q98eRcta8otmad9gyOSo/IaU5quwvTpLvubi/6MUmQGaRLP/gu+vOcxgQD3bnObkdPQhfyRXApEZ4sDTxRVo0cspA9kB2YVjYPnucWf/Kpg3Xx+d4NF0PDfVjAFITFtLLly+x3W4DZ/9gC3EYAtg+exxcMCykFTPRZGoMCEw7Xp3LhHnz5g1c18Wv7bbg7mMGDABt4XfiCtziJABxOl1Y9XQQwzAwOzvLgACwz7w+ffoE15XrHl4sarJGR2o0+lf0PcNId36RY48Bwyl6vDonFX4E8ezZs9D109PTzGeTYMoUUod7++j+9Sd2dg8SBzO+Yfp3rVZDrVYTgsmInLwMDOM7DUIEk1f8EheJ1b6y6xlR8/f7fWUQ33OBTrgwl16L//DhA5aWlgAEkQHk8xFapQRO2GdAcsF32u2i3QaLhrTwlAlfkfWlLfL223+x/e5d6BoNXlHAdruNabcLX/blKiAAsPX8KevX9fmZYMhPgG4q5lZKIKZtwd8btrD1/GlowsxbKyuDAMO5QdON0D7fw+UVTBvw99KXqJKktPIMADu7B6DSlJaxgPx7eUogJBqAqPSgrde0Kv+rgPD9P6oFRgIhmbYV24im2TuPfyiBkJ9ES1N+z3eUCjGXRSq6KVxJHKUwUwbRdINl4Lw17jxnpGWuQiq9h8urkQv33DvhE+iF1kVG6ZbcIA+XV8FaPNR3yAsFiQKMWg8DOXwkWhUShCi0vyqISPzQ/7+B8OUpWWNUR1UCES34AYE1qFtUj/PkAokqyUHzdo8SiKgR6hZKDfJaRQlEdkE4j1Wkz6HRPk70PEA0bPNaRckisplYRTexsLJc7Dk0Upp/FAEjDWLaVuLRQFJ0hVFFhYysfB7CrzCqpAaZkx6d2qSNaBI5qr+2WdJajQFV+LwKPWMko2iFL7pfGEjUP0zbArhDLXmPGJNG8pG880rhIJpuFAYjDUIrxRO1ycKrPCWQIs6spknpGClfxxQtqahxOl2YdnDKGxjuyxSRmUmD+GubpZ1WY7ABANyuBEHkLbqjkjr+BRT/XxSi+g+6WOpcpJGKCgAAAABJRU5ErkJggg==',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAABZCAYAAADVeL+8AAAFcElEQVR4nLVZzUtbWRT/JUqURAkkIWlBeIUmuDHQrqQI3Ygu/Qtk6LLMUldDqYtAF10ozKrMLjDC7LNsEMSdm7GQboIRfBDQ99BHxQ/aQM0sknM9971z33upmQOB5H78zu/ee+75uEks//EXoiTXqPWldm9tKxE2bzIO6MbqIqzqvNZ3tHeA+rDfpMQInmvU+huri6gsr2Lm+ROt7+bkHC+XXyNXLMFzHdQbtb6kQATPNWr9Pzd/w3RlAelCBhP5itY/A+AGgFUFLAxWJClImhinipYIrCZmZ5EqWoM5xZI8RmqsLK8imZ0VJ0hiVefx5sWzwMFr4MQ6TH5eHuPu4nYwOYJAYM+t6ryadHdxizSOtX4CjiOhphgFdn91DQCwW22xX9xzmhRXPNfBq7e/R4ObWJgIHO0diMAiuH9yWN/hP38r4O/HXwPjAnvuuU4sBYBu39KKReY91w4FpTHkb74ff9VIkWjMvbWtRL1R6+eKJVhVfWCqaAWU2q02LANrI3PPdSIPlvrtVltkLYJ7a1uJ+pfTgAJ+G6ndcx0jsJn5UAEHkoC51L+cBvy60RS9ta3EzudDIysOKgGHgo8ipkgUG3xUlwAACX+A5j6Z3K8/fpIc7R0AkPc7AJ5r1PpvXjxTN8+qzgfsm6IPtZEpkgFwJZMSMDFNFa1BOMNAAf1OFzK4Gc6jOCoFa+2GSsDpQgZ3AOAOxqQLGQADu7+/ulYrkYJ1krPmwCQT+YoC5G1cZp4/Qe7VoiJIoqyFGqVrz8Ek4Il8BRP5CqYrC1qwVuDcIvgB/rzUY6i/7ebkXP1OFzLaqjU7t1tt7Hw+DLDnYH5lPdfGzcm5WlUyO6t2QYFzjaMIEZFWOJbr7xfa4kjwuHnK3cVtgH0s5mEKRvbnJCZnZVLmb49kblLA200BfeQDlVhHBuieaxv3z88+rm8fmymSQu6HjOCmfZRYe66DnmurSoQu5KOYR2VmY9kWikIjgcfJGcPEmM5xBaSEK+u5dmTKZ2RumsiVRUkA/LI0j+2zqchkNCwBJRELrpWVFWw3m8DZN3xAUAGBbp9NAQDyQ2UUR2llxmpufX0djuPgXbMp9E4pEgDw7+4DAe52RfBSqYRyuYxyuYylpSUAQKfTEUk4jnlrRPByuSwO9rd3Oh2USnLdD7AD9R/e3Nyc+pgUmkgQlmiKBOj/7VcYJQo8yqyiCJBwf58EHsoUk48g6Xa7xj7Pdcxhjqe++/v7AVCylm63qz4mCyIJWMvm0x/Y3t0F8HBgBCKB+U2RF7xifv6hCLwbKiChC0OmR6DNZhN5BmxkzvPz3apeNYgXdbjSOiNvt9qqjNHA+dvW/dX1oERvAcApNp/+UAQAPSvmNSu3Os3OqZKg78CDiXJL4kUCT2BplWQcGjgFWH8lAehlOwEns7NIZmfx6eN7VezyV7rINy7OxP+qMZ1dUKv89PE9i06nD9tClsJLElPiQ2W73Wor6+BjxbKFREriRSXD7SFgKiUBBCsLXoXRNTaFOdp/u9VWMZUXw6RYYx6XtZ89MfcTCmwLdz6jekoSMgINnIDjZrH+1+fQS3R/da0dUFwxVYKhT3+mpw5ezoeVmGIF/RgxXn8uUXmgsuWQN/SxpNB+JxcA/5Xy3D8nVyxpjivgcklGtXGJ3CSdPAGTZzNZCpeeayMF/c1LA6cv6UJmpP8jPNcZRqm23uYH99+0qC0hv05+W+pX4L9i41FbBvxP7y2h4Ka/w8YCPi4RwcflZ4zMpT/yHgWeLmQwXVlAqmjh5fJrbKwuPkqBAh/lAsUVdUPvr64HD8LjBrdbbVhVaH4iToUcCe6tbSV2GrX+BgDyE6bH9lFFvfz7D+4xoCT/AaNCPNJBtKgMAAAAAElFTkSuQmCC',
'data:image/png;base64,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'
])
.setOriginX(0)
.setOriginY(0)
.setIsAnimated(true)
.setIsLooping(false)
.setFrameWidth(64)
.setFrameHeight(94)
.setFrameRate(7)
.setSprite(characterSprite)
.save()
const characterType = new CharacterType()
await characterType.setId('75b70c78-17f0-44c0-a4fa-15043cb95be0').setName('New character type').setGender(CharacterGender.MALE).setRace(CharacterRace.HUMAN).setIsSelectable(true).setSprite(characterSprite).save()
}
private async createCharacterHair(): Promise<void> {
const hairSprite = new Sprite()
hairSprite.setId('922ee95f-1500-49c0-8ead-f8cc46dad136').setName('Hair 1')
await hairSprite.save()
const frontAction = new SpriteAction()
await frontAction
.setAction('front')
.setSprites([
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA5ElEQVR4nK2VMQ7DIAxFc4AulViysTBUldgqReoGp+pReloqIhx9LOy4IoOVYBw//DFkyZ/vUs3db2Vkuc1rMTwOrRvUwPjwZXs9S0rv/d1BkuDXYx4N41SABAl+7ZJXPzcEqQALJApmroAnC22smbQPw5WPkjoAE7w++X6JAJSF9JRWS3EWiEnzwCrBWPJLkA4gdU1sPvoQIdjGGE+xp8mTULr1IO5Orjlt9NkGaif4kIgqwC6hSarMkkgFoLZ8hVvTeQoglXwJQNMV5bsUUJNKnZJnAHhVWO76vwCjH8hs8gr4ARPESmbfQUg4AAAAAElFTkSuQmCC'
])
.setOriginX(0.5)
.setOriginY(5.34)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64)
.setFrameHeight(18)
.setFrameRate(0)
.setSprite(hairSprite)
.save()
const backAction = new SpriteAction()
await backAction
.setAction('back')
.setSprites([
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAWCAYAAADafVyIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABHUlEQVR4nJ1VuwrDMAzMB3QpZOmWxUMpdCsYsrlflU/p16YIrCJfTrLTQZCHfSflLufpvX0mVvP1sns1sk7fu8DP+7Ln16MpeZaWWwMkz0pZf++xgQO4AssmLEsi1QN3J5DFXqUT4JRASRQoBYWkpybQ7lIFssCsASUb0gDBM3x7Bm6NQF3EwKWYuBa4p8nBPbgpB+CeqxoC9pPoBgau99ixR+I6yAPPRhtLwCZ3CbzPkquQVh9Zp3uty8IJLHgpa9OV3ishyyYbOeFPhnZFh0UEeu0SoIC5dm8JoojoRgXqUCq4JcB4GIoKpkMiE2EGDUUFngnoILQiOx+6Bw46IZFYQMJIB1ccL7pn+BH/EtkjsZONgA8TMIeMgAvBF10K1sbmijzuAAAAAElFTkSuQmCC'
])
.setOriginX(0.5)
.setOriginY(4.34)
.setIsAnimated(false)
.setIsLooping(false)
.setFrameWidth(64)
.setFrameHeight(22)
.setFrameRate(0)
.setSprite(hairSprite)
.save()
const characterHair = new CharacterHair()
await characterHair.setId('a2471230-d238-4ffb-9eca-9eab869f1b67').setName('Hair 1').setGender(CharacterGender.MALE).setIsSelectable(true).setSprite(hairSprite).save()
}
private async createCharacterEquipment(): Promise<void> {
const equipmentSprite = new Sprite()
equipmentSprite.id = '5b3932dd-0791-4bb7-bb1e-da9833c3cc50'
equipmentSprite.name = 'Male shirt'
// Create actions similar to createCharacterSprite()
// with appropriate sprite data and parameters
const actions = [
{
action: 'idle_right_down',
sprites: ['data:image/png;base64,...'],
originX: 0,
originY: 0,
isAnimated: false,
isLooping: false,
frameWidth: 64,
frameHeight: 94,
frameRate: 0
}
// Add other actions...
]
for (const actionData of actions) {
const action = new SpriteAction()
Object.assign(action, actionData)
action.sprite = equipmentSprite
await action.save()
}
await equipmentSprite.save()
}
private async createZone(): Promise<void> {
const zone = new Zone()
await zone
.setName('New zone')
.setWidth(100)
.setHeight(100)
.setTiles(Array.from({ length: 100 }, () => Array.from({ length: 100 }, () => 'a2fd8d6f-5042-437a-9c1e-c66b91ecc35b')))
.save()
const effect = new ZoneEffect()
await effect.setEffect('light').setStrength(100).setZone(zone).save()
}
private async createUser(): Promise<void> {
const user = new User()
await user.setId('6f9a58b4-172d-425e-b9ea-71e1d13d81ee').setUsername('root').setEmail('local@host').setPassword('password').setOnline(false).save()
const character = new Character()
await character
.setId('26850183-1757-4135-938f-aa1448c49654')
.setUser(user)
.setName('root')
.setRole('gm')
.setZone((await ZoneRepository.getFirst())!)
.setCharacterType((await CharacterTypeRepository.getFirst()) ?? undefined)
.setCharacterHair((await CharacterHairRepository.getFirst()) ?? undefined)
.save()
}
}

View File

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

56
src/commands/tiles.ts Normal file
View File

@ -0,0 +1,56 @@
import fs from 'fs'
import sharp from 'sharp'
import { BaseCommand } from '#application/base/baseCommand'
import Storage from '#application/storage'
export default class TilesCommand extends BaseCommand {
public async execute(): Promise<void> {
// Get all tiles
const tilesDir = Storage.getPublicPath('tiles')
const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png'))
// Create output directory if it doesn't exist
if (!fs.existsSync(tilesDir)) {
fs.mkdirSync(tilesDir, { recursive: true })
}
for (const tile of tiles) {
// Check if tile is already 66x34
const metadata = await sharp(Storage.getPublicPath('tiles', tile)).metadata()
if (metadata.width === 66 && metadata.height === 34) {
this.logger.info(`Tile ${tile} already processed`)
continue
}
const inputPath = Storage.getPublicPath('tiles', tile)
const tempPath = Storage.getPublicPath('tiles', `temp_${tile}`)
try {
await sharp(inputPath)
.resize({
width: 66,
height: 34,
fit: 'fill',
kernel: 'nearest'
})
.toFile(tempPath)
// Replace original file with processed file
fs.unlinkSync(inputPath)
fs.renameSync(tempPath, inputPath)
this.logger.info(`Processed and replaced: ${tile}`)
} catch (error) {
console.error(`Error processing ${tile}:`, error)
// Clean up temp file if it exists
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath)
}
}
}
this.logger.info('Tile processing completed.')
}
}

297
src/entities/character.ts Normal file
View File

@ -0,0 +1,297 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { CharacterEquipment } from './characterEquipment'
import { CharacterHair } from './characterHair'
import { CharacterItem } from './characterItem'
import { CharacterType } from './characterType'
import { Chat } from './chat'
import { User } from './user'
import { Zone } from './zone'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class Character extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
user!: User
@Property({ unique: true })
name!: string
@Property()
online = false
@Property()
role = 'player'
@OneToMany(() => Chat, (chat) => chat.character)
chats = new Collection<Chat>(this)
// Position
@ManyToOne()
zone!: Zone // @TODO: Update to spawn point when current zone is not found
@Property()
positionX = 0
@Property()
positionY = 0
@Property()
rotation = 0
// Customization
@ManyToOne({ deleteRule: 'set null' })
characterType?: CharacterType | null | undefined
@ManyToOne({ deleteRule: 'set null' })
characterHair?: CharacterHair | null | undefined
// Inventory
@OneToMany({ mappedBy: 'character' })
items = new Collection<CharacterItem>(this)
@OneToMany({ mappedBy: 'character' })
equipment = new Collection<CharacterEquipment>(this)
// Stats
@Property()
alignment = 50
@Property()
hitpoints = 100
@Property()
mana = 100
@Property()
level = 1
@Property()
experience = 0
@Property()
strength = 10
@Property()
dexterity = 10
@Property()
intelligence = 10
@Property()
wisdom = 10
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setUser(user: User) {
this.user = user
return this
}
getUser() {
return this.user
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setOnline(online: boolean) {
this.online = online
return this
}
getOnline() {
return this.online
}
setRole(role: string) {
this.role = role
return this
}
getRole() {
return this.role
}
setChats(chats: Collection<Chat>) {
this.chats = chats
return this
}
getChats() {
return this.chats
}
setZone(zone: Zone) {
this.zone = zone
return this
}
getZone() {
return this.zone
}
setPositionX(positionX: number) {
this.positionX = positionX
return this
}
getPositionX() {
return this.positionX
}
setPositionY(positionY: number) {
this.positionY = positionY
return this
}
getPositionY() {
return this.positionY
}
setRotation(rotation: number) {
this.rotation = rotation
return this
}
getRotation() {
return this.rotation
}
setCharacterType(characterType: CharacterType | null | undefined) {
this.characterType = characterType
return this
}
getCharacterType() {
return this.characterType
}
setCharacterHair(characterHair: CharacterHair | null | undefined) {
this.characterHair = characterHair
return this
}
getCharacterHair() {
return this.characterHair
}
setItems(items: Collection<CharacterItem>) {
this.items = items
return this
}
getItems() {
return this.items
}
setEquipment(equipment: Collection<CharacterEquipment>) {
this.equipment = equipment
return this
}
getEquipment() {
return this.equipment
}
setAlignment(alignment: number) {
this.alignment = alignment
return this
}
getAlignment() {
return this.alignment
}
setHitpoints(hitpoints: number) {
this.hitpoints = hitpoints
return this
}
getHitpoints() {
return this.hitpoints
}
setMana(mana: number) {
this.mana = mana
return this
}
getMana() {
return this.mana
}
setLevel(level: number) {
this.level = level
return this
}
getLevel() {
return this.level
}
setExperience(experience: number) {
this.experience = experience
return this
}
getExperience() {
return this.experience
}
setStrength(strength: number) {
this.strength = strength
return this
}
getStrength() {
return this.strength
}
setDexterity(dexterity: number) {
this.dexterity = dexterity
return this
}
getDexterity() {
return this.dexterity
}
setIntelligence(intelligence: number) {
this.intelligence = intelligence
return this
}
getIntelligence() {
return this.intelligence
}
setWisdom(wisdom: number) {
this.wisdom = wisdom
return this
}
getWisdom() {
return this.wisdom
}
}

View File

@ -0,0 +1,61 @@
import { randomUUID } from 'node:crypto'
import { Entity, Enum, ManyToOne, PrimaryKey } from '@mikro-orm/core'
import { Character } from './character'
import { CharacterItem } from './characterItem'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterEquipmentSlotType } from '#application/enums'
import { UUID } from '#application/types'
@Entity()
export class CharacterEquipment extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Enum(() => CharacterEquipmentSlotType)
slot!: CharacterEquipmentSlotType
@ManyToOne({ deleteRule: 'cascade' })
character!: Character
@ManyToOne({ deleteRule: 'cascade' })
characterItem!: CharacterItem
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setSlot(slot: CharacterEquipmentSlotType) {
this.slot = slot
return this
}
getSlot() {
return this.slot
}
setCharacter(character: Character) {
this.character = character
return this
}
getCharacter() {
return this.character
}
setCharacterItem(characterItem: CharacterItem) {
this.characterItem = characterItem
return this
}
getCharacterItem() {
return this.characterItem
}
}

View File

@ -0,0 +1,73 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { Character } from './character'
import { Sprite } from './sprite'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterGender } from '#application/enums'
import { UUID } from '#application/types'
@Entity()
export class CharacterHair extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property()
gender: CharacterGender = CharacterGender.MALE
@Property()
isSelectable = false
@ManyToOne({ nullable: true })
sprite?: Sprite
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setGender(gender: CharacterGender) {
this.gender = gender
return this
}
getGender() {
return this.gender
}
setIsSelectable(isSelectable: boolean) {
this.isSelectable = isSelectable
return this
}
getIsSelectable() {
return this.isSelectable
}
setSprite(sprite: Sprite) {
this.sprite = sprite
return this
}
getSprite() {
return this.sprite
}
}

View File

@ -0,0 +1,61 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { Character } from './character'
import { CharacterEquipment } from './characterEquipment'
import { Item } from './item'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class CharacterItem extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
character!: Character
@ManyToOne({ deleteRule: 'cascade' })
item!: Item
@Property()
quantity!: number
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setCharacter(character: Character) {
this.character = character
return this
}
getCharacter() {
return this.character
}
setItem(item: Item) {
this.item = item
return this
}
getItem() {
return this.item
}
setQuantity(quantity: number) {
this.quantity = quantity
return this
}
getQuantity() {
return this.quantity
}
}

View File

@ -0,0 +1,121 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { Character } from './character'
import { Sprite } from './sprite'
import { BaseEntity } from '#application/base/baseEntity'
import { CharacterGender, CharacterRace } from '#application/enums'
import { UUID } from '#application/types'
@Entity()
export class CharacterType extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Enum(() => CharacterGender)
gender!: CharacterGender
@Enum(() => CharacterRace)
race!: CharacterRace
@Property()
isSelectable = false
@OneToMany(() => Character, (character) => character.characterType)
characters = new Collection<Character>(this)
@ManyToOne(() => Sprite, { nullable: true })
sprite?: Sprite
@Property()
createdAt = new Date()
@Property()
updatedAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setGender(gender: CharacterGender) {
this.gender = gender
return this
}
getGender() {
return this.gender
}
setRace(race: CharacterRace) {
this.race = race
return this
}
getRace() {
return this.race
}
setIsSelectable(isSelectable: boolean) {
this.isSelectable = isSelectable
return this
}
getIsSelectable() {
return this.isSelectable
}
setSprite(sprite: Sprite) {
this.sprite = sprite
return this
}
getSprite() {
return this.sprite
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
setUpdatedAt(updatedAt: Date) {
this.updatedAt = updatedAt
return this
}
getUpdatedAt() {
return this.updatedAt
}
setCharacters(characters: Collection<Character>) {
this.characters = characters
return this
}
getCharacters() {
return this.characters
}
}

72
src/entities/chat.ts Normal file
View File

@ -0,0 +1,72 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Character } from './character'
import { Zone } from './zone'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class Chat extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
character!: Character
@ManyToOne({ deleteRule: 'cascade' })
zone!: Zone
@Property()
message!: string
@Property()
createdAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setCharacter(character: Character) {
this.character = character
return this
}
getCharacter() {
return this.character
}
setZone(zone: Zone) {
this.zone = zone
return this
}
getZone() {
return this.zone
}
setMessage(message: string) {
this.message = message
return this
}
getMessage() {
return this.message
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
}

121
src/entities/item.ts Normal file
View File

@ -0,0 +1,121 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { CharacterItem } from './characterItem'
import { Sprite } from './sprite'
import { BaseEntity } from '#application/base/baseEntity'
import { ItemType, ItemRarity } from '#application/enums'
import { UUID } from '#application/types'
@Entity()
export class Item extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property({ nullable: true })
description?: string
@Enum(() => ItemType)
itemType!: ItemType
@Property()
stackable = false
@Enum(() => ItemRarity)
rarity: ItemRarity = ItemRarity.COMMON
@ManyToOne(() => Sprite, { nullable: true })
sprite?: Sprite
@Property()
createdAt = new Date()
@Property()
updatedAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setDescription(description: string) {
this.description = description
return this
}
getDescription() {
return this.description
}
setItemType(itemType: ItemType) {
this.itemType = itemType
return this
}
getItemType() {
return this.itemType
}
setStackable(stackable: boolean) {
this.stackable = stackable
return this
}
getStackable() {
return this.stackable
}
setRarity(rarity: ItemRarity) {
this.rarity = rarity
return this
}
getRarity() {
return this.rarity
}
setSprite(sprite: Sprite) {
this.sprite = sprite
return this
}
getSprite() {
return this.sprite
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
setUpdatedAt(updatedAt: Date) {
this.updatedAt = updatedAt
return this
}
getUpdatedAt() {
return this.updatedAt
}
}

143
src/entities/mapObject.ts Normal file
View File

@ -0,0 +1,143 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { ZoneObject } from './zoneObject'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class MapObject extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property({ type: 'json', nullable: true })
tags?: any
@Property()
originX = 0
@Property()
originY = 0
@Property()
isAnimated = false
@Property()
frameRate = 0
@Property()
frameWidth = 0
@Property()
frameHeight = 0
@Property()
createdAt = new Date()
@Property()
updatedAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setTags(tags: any) {
this.tags = tags
return this
}
getTags() {
return this.tags
}
setOriginX(originX: number) {
this.originX = originX
return this
}
getOriginX() {
return this.originX
}
setOriginY(originY: number) {
this.originY = originY
return this
}
getOriginY() {
return this.originY
}
setIsAnimated(isAnimated: boolean) {
this.isAnimated = isAnimated
return this
}
getIsAnimated() {
return this.isAnimated
}
setFrameRate(frameRate: number) {
this.frameRate = frameRate
return this
}
getFrameRate() {
return this.frameRate
}
setFrameWidth(frameWidth: number) {
this.frameWidth = frameWidth
return this
}
getFrameWidth() {
return this.frameWidth
}
setFrameHeight(frameHeight: number) {
this.frameHeight = frameHeight
return this
}
getFrameHeight() {
return this.frameHeight
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
setUpdatedAt(updatedAt: Date) {
this.updatedAt = updatedAt
return this
}
getUpdatedAt() {
return this.updatedAt
}
}

View File

@ -0,0 +1,59 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { User } from './user'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class PasswordResetToken extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
user!: User
@Property({ unique: true })
token!: string
@Property()
createdAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setUser(user: User) {
this.user = user
return this
}
getUser() {
return this.user
}
setToken(token: string) {
this.token = token
return this
}
getToken() {
return this.token
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
}

71
src/entities/sprite.ts Normal file
View File

@ -0,0 +1,71 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { SpriteAction } from './spriteAction'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class Sprite extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@OneToMany(() => SpriteAction, (action) => action.sprite)
spriteActions = new Collection<SpriteAction>(this)
@Property()
createdAt = new Date()
@Property()
updatedAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setSpriteActions(spriteActions: Collection<SpriteAction>) {
this.spriteActions = spriteActions
return this
}
getSpriteActions() {
return this.spriteActions
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
setUpdatedAt(updatedAt: Date) {
this.updatedAt = updatedAt
return this
}
getUpdatedAt() {
return this.updatedAt
}
}

View File

@ -0,0 +1,143 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Sprite } from './sprite'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class SpriteAction extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
sprite!: Sprite
@Property()
action!: string
@Property({ type: 'json', nullable: true })
sprites?: string[]
@Property()
originX = 0
@Property()
originY = 0
@Property()
isAnimated = false
@Property()
isLooping = false
@Property()
frameWidth = 0
@Property()
frameHeight = 0
@Property()
frameRate = 0
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setSprite(sprite: Sprite) {
this.sprite = sprite
return this
}
getSprite() {
return this.sprite
}
setAction(action: string) {
this.action = action
return this
}
getAction() {
return this.action
}
setSprites(sprites: string[]) {
this.sprites = sprites
return this
}
getSprites() {
return this.sprites
}
setOriginX(originX: number) {
this.originX = originX
return this
}
getOriginX() {
return this.originX
}
setOriginY(originY: number) {
this.originY = originY
return this
}
getOriginY() {
return this.originY
}
setIsAnimated(isAnimated: boolean) {
this.isAnimated = isAnimated
return this
}
getIsAnimated() {
return this.isAnimated
}
setIsLooping(isLooping: boolean) {
this.isLooping = isLooping
return this
}
getIsLooping() {
return this.isLooping
}
setFrameWidth(frameWidth: number) {
this.frameWidth = frameWidth
return this
}
getFrameWidth() {
return this.frameWidth
}
setFrameHeight(frameHeight: number) {
this.frameHeight = frameHeight
return this
}
getFrameHeight() {
return this.frameHeight
}
setFrameRate(frameRate: number) {
this.frameRate = frameRate
return this
}
getFrameRate() {
return this.frameRate
}
}

69
src/entities/tile.ts Normal file
View File

@ -0,0 +1,69 @@
import { randomUUID } from 'node:crypto'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class Tile extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property({ type: 'json', nullable: true })
tags?: any
@Property()
createdAt = new Date()
@Property()
updatedAt = new Date()
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setTags(tags: any) {
this.tags = tags
return this
}
getTags() {
return this.tags
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
setUpdatedAt(updatedAt: Date) {
this.updatedAt = updatedAt
return this
}
getUpdatedAt() {
return this.updatedAt
}
}

98
src/entities/user.ts Normal file
View File

@ -0,0 +1,98 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import bcrypt from 'bcryptjs'
import { Character } from './character'
import { PasswordResetToken } from './passwordResetToken'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class User extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property({ unique: true })
username!: string
@Property({ unique: true })
email!: string
@Property()
password!: string
@Property()
online = false
@OneToMany(() => Character, (character) => character.user)
characters = new Collection<Character>(this)
@OneToMany(() => PasswordResetToken, (token) => token.user)
passwordResetTokens = new Collection<PasswordResetToken>(this)
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setUsername(username: string) {
this.username = username
return this
}
getUsername() {
return this.username
}
setEmail(email: string) {
this.email = email
return this
}
getEmail() {
return this.email
}
setPassword(password: string) {
this.password = bcrypt.hashSync(password, 10)
return this
}
getPassword() {
return this.password
}
setOnline(online: boolean) {
this.online = online
return this
}
getOnline() {
return this.online
}
setCharacters(characters: Collection<Character>) {
this.characters = characters
return this
}
getCharacters() {
return this.characters
}
setPasswordResetTokens(passwordResetTokens: Collection<PasswordResetToken>) {
this.passwordResetTokens = passwordResetTokens
return this
}
getPasswordResetTokens() {
return this.passwordResetTokens
return this
}
}

66
src/entities/world.ts Normal file
View File

@ -0,0 +1,66 @@
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/base/baseEntity'
@Entity()
export class World extends BaseEntity {
@PrimaryKey()
date = new Date()
@Property()
isRainEnabled = false
@Property()
rainPercentage = 0
@Property()
isFogEnabled = false
@Property()
fogDensity = 0
setDate(date: Date) {
this.date = date
return this
}
getDate() {
return this.date
}
setIsRainEnabled(isRainEnabled: boolean) {
this.isRainEnabled = isRainEnabled
return this
}
getIsRainEnabled() {
return this.isRainEnabled
}
setRainPercentage(rainPercentage: number) {
this.rainPercentage = rainPercentage
return this
}
getRainPercentage() {
return this.rainPercentage
}
setIsFogEnabled(isFogEnabled: boolean) {
this.isFogEnabled = isFogEnabled
return this
}
getIsFogEnabled() {
return this.isFogEnabled
}
setFogDensity(fogDensity: number) {
this.fogDensity = fogDensity
return this
}
getFogDensity() {
return this.fogDensity
}
}

184
src/entities/zone.ts Normal file
View File

@ -0,0 +1,184 @@
import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { Character } from './character'
import { Chat } from './chat'
import { ZoneEffect } from './zoneEffect'
import { ZoneEventTile } from './zoneEventTile'
import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
import { ZoneObject } from './zoneObject'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class Zone extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@Property()
name!: string
@Property()
width = 10
@Property()
height = 10
@Property({ type: 'json', nullable: true })
tiles?: any
@Property()
pvp = false
@Property()
createdAt = new Date()
@Property()
updatedAt = new Date()
@OneToMany(() => ZoneEffect, (effect) => effect.zone)
zoneEffects = new Collection<ZoneEffect>(this)
@OneToMany(() => ZoneEventTile, (tile) => tile.zone)
zoneEventTiles = new Collection<ZoneEventTile>(this)
@OneToMany(() => ZoneEventTileTeleport, (teleport) => teleport.toZone)
zoneEventTileTeleports = new Collection<ZoneEventTileTeleport>(this)
@OneToMany(() => ZoneObject, (object) => object.zone)
zoneObjects = new Collection<ZoneObject>(this)
@OneToMany(() => Character, (character) => character.zone)
characters = new Collection<Character>(this)
@OneToMany(() => Chat, (chat) => chat.zone)
chats = new Collection<Chat>(this)
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setName(name: string) {
this.name = name
return this
}
getName() {
return this.name
}
setWidth(width: number) {
this.width = width
return this
}
getWidth() {
return this.width
}
setHeight(height: number) {
this.height = height
return this
}
getHeight() {
return this.height
}
setTiles(tiles: any) {
this.tiles = tiles
return this
}
getTiles() {
return this.tiles
}
setPvp(pvp: boolean) {
this.pvp = pvp
return this
}
getPvp() {
return this.pvp
}
setCreatedAt(createdAt: Date) {
this.createdAt = createdAt
return this
}
getCreatedAt() {
return this.createdAt
}
setUpdatedAt(updatedAt: Date) {
this.updatedAt = updatedAt
return this
}
getUpdatedAt() {
return this.updatedAt
}
setZoneEffects(zoneEffects: Collection<ZoneEffect>) {
this.zoneEffects = zoneEffects
return this
}
getZoneEffects() {
return this.zoneEffects
}
setZoneEventTiles(zoneEventTiles: Collection<ZoneEventTile>) {
this.zoneEventTiles = zoneEventTiles
return this
}
getZoneEventTiles() {
return this.zoneEventTiles
}
setZoneEventTileTeleports(zoneEventTileTeleports: Collection<ZoneEventTileTeleport>) {
this.zoneEventTileTeleports = zoneEventTileTeleports
return this
}
getZoneEventTileTeleports() {
return this.zoneEventTileTeleports
}
setZoneObjects(zoneObjects: Collection<ZoneObject>) {
this.zoneObjects = zoneObjects
return this
}
getZoneObjects() {
return this.zoneObjects
}
setCharacters(characters: Collection<Character>) {
this.characters = characters
return this
}
getCharacters() {
return this.characters
}
setChats(chats: Collection<Chat>) {
this.chats = chats
return this
}
getChats() {
return this.chats
}
}

View File

@ -0,0 +1,59 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Zone } from './zone'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class ZoneEffect extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
zone!: Zone
@Property()
effect!: string
@Property()
strength!: number
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setZone(zone: Zone) {
this.zone = zone
return this
}
getZone() {
return this.zone
}
setEffect(effect: string) {
this.effect = effect
return this
}
getEffect() {
return this.effect
}
setStrength(strength: number) {
this.strength = strength
return this
}
getStrength() {
return this.strength
}
}

View File

@ -0,0 +1,85 @@
import { randomUUID } from 'node:crypto'
import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Zone } from './zone'
import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
import { BaseEntity } from '#application/base/baseEntity'
import { ZoneEventTileType } from '#application/enums'
import { UUID } from '#application/types'
@Entity()
export class ZoneEventTile extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
zone!: Zone
@Enum(() => ZoneEventTileType)
type!: ZoneEventTileType
@Property()
positionX!: number
@Property()
positionY!: number
@OneToOne(() => ZoneEventTileTeleport, (teleport) => teleport.zoneEventTile)
teleport?: ZoneEventTileTeleport
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setZone(zone: Zone) {
this.zone = zone
return this
}
getZone() {
return this.zone
}
setType(type: ZoneEventTileType) {
this.type = type
return this
}
getType() {
return this.type
}
setPositionX(positionX: number) {
this.positionX = positionX
return this
}
getPositionX() {
return this.positionX
}
setPositionY(positionY: number) {
this.positionY = positionY
return this
}
getPositionY() {
return this.positionY
}
setTeleport(teleport: ZoneEventTileTeleport) {
this.teleport = teleport
return this
}
getTeleport() {
return this.teleport
}
}

View File

@ -0,0 +1,84 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Zone } from './zone'
import { ZoneEventTile } from './zoneEventTile'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
@Entity()
export class ZoneEventTileTeleport extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@OneToOne({ deleteRule: 'cascade' })
zoneEventTile!: ZoneEventTile
@ManyToOne({ deleteRule: 'cascade' })
toZone!: Zone
@Property()
toRotation!: number
@Property()
toPositionX!: number
@Property()
toPositionY!: number
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setZoneEventTile(zoneEventTile: ZoneEventTile) {
this.zoneEventTile = zoneEventTile
return this
}
getZoneEventTile() {
return this.zoneEventTile
}
setToZone(toZone: Zone) {
this.toZone = toZone
return this
}
getToZone() {
return this.toZone
}
setToRotation(toRotation: number) {
this.toRotation = toRotation
return this
}
getToRotation() {
return this.toRotation
}
setToPositionX(toPositionX: number) {
this.toPositionX = toPositionX
return this
}
getToPositionX() {
return this.toPositionX
}
setToPositionY(toPositionY: number) {
this.toPositionY = toPositionY
return this
}
getToPositionY() {
return this.toPositionY
}
}

View File

@ -0,0 +1,97 @@
import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { Zone } from './zone'
import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types'
import { MapObject } from '#entities/mapObject'
//@TODO : Rename mapObject
@Entity()
export class ZoneObject extends BaseEntity {
@PrimaryKey()
id = randomUUID()
@ManyToOne({ deleteRule: 'cascade' })
zone!: Zone
@ManyToOne({ deleteRule: 'cascade' })
mapObject!: MapObject
@Property()
depth = 0
@Property()
isRotated = false
@Property()
positionX = 0
@Property()
positionY = 0
setId(id: UUID) {
this.id = id
return this
}
getId() {
return this.id
}
setZone(zone: Zone) {
this.zone = zone
return this
}
getZone() {
return this.zone
}
setMapObject(mapObject: MapObject) {
this.mapObject = mapObject
return this
}
getMapObject() {
return this.mapObject
}
setDepth(depth: number) {
this.depth = depth
return this
}
getDepth() {
return this.depth
}
setIsRotated(isRotated: boolean) {
this.isRotated = isRotated
return this
}
getIsRotated() {
return this.isRotated
}
setPositionX(positionX: number) {
this.positionX = positionX
return this
}
getPositionX() {
return this.positionX
}
setPositionY(positionY: number) {
this.positionY = positionY
return this
}
getPositionY() {
return this.positionY
}
}

View File

@ -0,0 +1,18 @@
import { BaseEvent } from '#application/base/baseEvent'
import Database from '#application/database'
import { CharacterHair } from '#entities/characterHair'
import characterHairRepository from '#repositories/characterHairRepository'
interface IPayload {}
export default class characterHairListEvent extends BaseEvent {
public listen(): void {
this.socket.on('character:hair:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
const items: CharacterHair[] = await characterHairRepository.getAllSelectable()
await Database.getEntityManager().populate(items, ['sprite'])
callback(items)
}
}

View File

@ -0,0 +1,80 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import ZoneManager from '#managers/zoneManager'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository'
import TeleportService from '#services/teleportService'
interface CharacterConnectPayload {
characterId: UUID
characterHairId?: UUID
}
export default class CharacterConnectEvent extends BaseEvent {
public listen(): void {
this.socket.on('character:connect', this.handleEvent.bind(this))
}
/**
* Handle character connect event
* @TODO:
* 1. Check if character is already connected
* 2. Update character hair if provided
* 3. Emit character connect event
* 4. Let other clients know of new character
* @param data
* @param callback
* @private
*/
private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {
if (!this.socket.userId) {
this.emitError('User not authenticated')
return
}
try {
if (await this.checkForActiveCharacters()) {
this.emitError('You are already connected to another character')
return
}
const character = await CharacterRepository.getByUserAndId(this.socket.userId, data.characterId)
if (!character) {
this.emitError('Character not found or does not belong to this user')
return
}
// Set character id
this.socket.characterId = character.id
// Set character hair
if (data.characterHairId !== undefined && data.characterHairId !== null) {
const characterHair = await CharacterHairRepository.getById(data.characterHairId)
await character.setCharacterHair(characterHair).update()
}
// Emit character connect event
callback({ character })
// wait 300 ms, @TODO: Find a better way to do this
await new Promise((resolve) => setTimeout(resolve, 100))
await TeleportService.teleportCharacter(character.id, {
targetZoneId: character.zone.id,
targetX: character.positionX,
targetY: character.positionY,
rotation: character.rotation,
isInitialJoin: true,
character
})
} catch (error) {
this.handleError('Failed to connect character', error)
}
}
private async checkForActiveCharacters(): Promise<boolean> {
const characters = await CharacterRepository.getByUserId(this.socket.userId!)
return characters?.some((char) => ZoneManager.getCharacterById(char.id)) ?? false
}
}

View File

@ -0,0 +1,63 @@
import { ZodError } from 'zod'
import { BaseEvent } from '#application/base/baseEvent'
import { ZCharacterCreate } from '#application/zodTypes'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
import UserRepository from '#repositories/userRepository'
import ZoneRepository from '#repositories/zoneRepository'
export default class CharacterCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on('character:create', this.handleEvent.bind(this))
}
private async handleEvent(data: any): Promise<any> {
// zod validate
try {
data = ZCharacterCreate.parse(data)
const user = await UserRepository.getById(this.socket.userId!)
if (!user) {
return this.socket.emit('notification', { message: 'User not found' })
}
// Check if character name already exists
const characterExists = await CharacterRepository.getByName(data.name)
if (characterExists) {
return this.socket.emit('notification', { message: 'Character name already exists' })
}
let characters: Character[] = await CharacterRepository.getByUserId(user.getId())
if (characters.length >= 4) {
return this.socket.emit('notification', { message: 'You can only have 4 characters' })
}
// @TODO: Change to default location
const zone = await ZoneRepository.getFirst()
const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setZone(zone!).save()
if (!newCharacter) {
return this.socket.emit('notification', { message: 'Failed to create character. Please try again (later).' })
}
characters = [...characters, newCharacter]
this.socket.emit('character:create:success')
this.socket.emit('character:list', characters)
this.logger.info('character:create success')
} catch (error: any) {
this.logger.error(`character:create error: ${error.message}`)
if (error instanceof ZodError) {
return this.socket.emit('notification', { message: error.issues[0].message })
}
return this.socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
}
}
}

View File

@ -0,0 +1,35 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import { Character } from '#entities/character'
import { Zone } from '#entities/zone'
import CharacterRepository from '#repositories/characterRepository'
type TypePayload = {
characterId: UUID
}
type TypeResponse = {
zone: Zone
characters: Character[]
}
export default class CharacterDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('character:delete', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
try {
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId)
if (character) {
await character.delete()
}
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
this.socket.emit('character:list', characters)
} catch (error: any) {
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
}
}
}

View File

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

View File

@ -0,0 +1,46 @@
import { BaseEvent } from '#application/base/baseEvent'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
}
export default class AlertCommandEvent extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!ChatService.isCommand(data.message, 'alert')) {
return
}
// Check if character exists
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
if (!character) {
this.logger.error('chat:alert_command error', 'Character not found')
return callback(false)
}
// Check if the user is the GM
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
return callback(false)
}
const args = ChatService.getArgs('alert', data.message)
if (!args) {
return callback(false)
}
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
return callback(true)
} catch (error: any) {
this.logger.error('chat:alert_command error', error.message)
callback(false)
}
}
}

View File

@ -0,0 +1,53 @@
import { BaseEvent } from '#application/base/baseEvent'
import DateManager from '#managers/dateManager'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
}
export default class SetTimeCommand extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!ChatService.isCommand(data.message, 'time')) {
return
}
// Check if character exists
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
if (!character) {
this.logger.error('chat:alert_command error', 'Character not found')
return
}
// Check if the user is the GM
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
return
}
// Get arguments
const args = ChatService.getArgs('time', data.message)
if (!args) {
return
}
const time = args[0] // 24h time, e.g. 17:34
if (!time) {
return
}
await DateManager.setTime(time)
} catch (error: any) {
this.logger.error('command error', error.message)
callback(false)
}
}
}

View File

@ -0,0 +1,100 @@
import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types'
import ZoneManager from '#managers/zoneManager'
import ZoneRepository from '#repositories/zoneRepository'
import ChatService from '#services/chatService'
import TeleportService from '#services/teleportService'
type TypePayload = {
message: string
}
export default class TeleportCommandEvent extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void) {
try {
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
if (!zoneCharacter) {
this.logger.error('chat:message error', 'Character not found')
return
}
const character = zoneCharacter.character
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
return
}
if (!ChatService.isCommand(data.message, 'teleport')) return
const args = ChatService.getArgs('teleport', data.message)
if (!args || args.length === 0 || args.length > 3) {
this.socket.emit('notification', {
title: 'Server message',
message: 'Usage: /teleport <zoneId> [x] [y]'
})
return
}
const zoneId = args[0] as UUID
const targetX = args[1] ? parseInt(args[1], 10) : 0
const targetY = args[2] ? parseInt(args[2], 10) : 0
if (!zoneId || isNaN(targetX) || isNaN(targetY)) {
this.socket.emit('notification', {
title: 'Server message',
message: 'Invalid parameters. X and Y coordinates must be numbers.'
})
return
}
const zone = await ZoneRepository.getById(zoneId)
if (!zone) {
this.socket.emit('notification', {
title: 'Server message',
message: 'Zone not found'
})
return
}
if (character.zone.id === zone.id && targetX === character.positionX && targetY === character.positionY) {
this.socket.emit('notification', {
title: 'Server message',
message: 'You are already at that location'
})
return
}
const success = await TeleportService.teleportCharacter(character.id, {
targetZoneId: zone.id,
targetX,
targetY,
rotation: character.rotation
})
if (!success) {
return this.socket.emit('notification', {
title: 'Server message',
message: 'Failed to teleport'
})
}
this.socket.emit('notification', {
title: 'Server message',
message: `Teleported to ${zone.name} (${targetX}, ${targetY})`
})
this.logger.info('teleport', `Character ${character.id} teleported to zone ${zone.id} at position (${targetX}, ${targetY})`)
} catch (error: any) {
this.logger.error(`Error in teleport command: ${error.message}`)
this.socket.emit('notification', {
title: 'Server message',
message: 'An error occurred while teleporting'
})
}
}
}

View File

@ -0,0 +1,40 @@
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
}
export default class ToggleFogCommand extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!ChatService.isCommand(data.message, 'fog')) {
return
}
// Check if character exists
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
if (!character) {
this.logger.error('chat:alert_command error', 'Character not found')
return
}
// Check if the user is the GM
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
return
}
await WeatherManager.toggleFog()
} catch (error: any) {
this.logger.error('command error', error.message)
callback(false)
}
}
}

View File

@ -0,0 +1,40 @@
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
}
export default class ToggleRainCommand extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!ChatService.isCommand(data.message, 'rain')) {
return
}
// Check if character exists
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, this.socket.characterId!)
if (!character) {
this.logger.error('chat:alert_command error', 'Character not found')
return
}
// Check if the user is the GM
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to set time but is not a game master.`)
return
}
await WeatherManager.toggleRain()
} catch (error: any) {
this.logger.error('command error', error.message)
callback(false)
}
}
}

View File

@ -0,0 +1,45 @@
import { BaseEvent } from '#application/base/baseEvent'
import ZoneManager from '#managers/zoneManager'
import ZoneRepository from '#repositories/zoneRepository'
import ChatService from '#services/chatService'
type TypePayload = {
message: string
}
export default class ChatMessageEvent extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
try {
if (!data.message || ChatService.isCommand(data.message)) {
return callback(false)
}
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
if (!zoneCharacter) {
this.logger.error('chat:message error', 'Character not found')
return callback(false)
}
const character = zoneCharacter.character
const zone = await ZoneRepository.getById(character.zone.id)
if (!zone) {
this.logger.error('chat:message error', 'Zone not found')
return callback(false)
}
if (await ChatService.sendZoneMessage(character.getId(), zone.getId(), data.message)) {
return callback(true)
}
callback(false)
} catch (error: any) {
this.logger.error('chat:message error', error.message)
callback(false)
}
}
}

30
src/events/disconnect.ts Normal file
View File

@ -0,0 +1,30 @@
import { BaseEvent } from '#application/base/baseEvent'
import ZoneManager from '#managers/zoneManager'
export default class DisconnectEvent extends BaseEvent {
public listen(): void {
this.socket.on('disconnect', this.handleEvent.bind(this))
}
private async handleEvent(): Promise<void> {
try {
if (!this.socket.userId) {
this.logger.info('User disconnected but had no user set')
return
}
this.io.emit('user:disconnect', this.socket.userId)
const zoneCharacter = ZoneManager.getCharacterById(this.socket.characterId!)
if (!zoneCharacter) {
this.logger.info('User disconnected but had no character set')
return
}
await zoneCharacter.disconnect(this.socket, this.io)
this.logger.info('User disconnected along with their character')
} catch (error: any) {
this.logger.error('disconnect error: ' + error.message)
}
}
}

View File

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

View File

@ -0,0 +1,34 @@
import { BaseEvent } from '#application/base/baseEvent'
import CharacterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
interface IPayload {
id: number
}
export default class characterHairDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
const characterHair = await CharacterHairRepository.getById(data.id)
if (characterHair) {
await characterHair.delete()
}
callback(true)
} catch (error) {
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
}
}
}

View File

@ -0,0 +1,29 @@
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair'
import characterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
interface IPayload {}
export default class characterHairListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) {
this.logger.error('gm:characterHair:list error', 'Character not found')
return callback([])
}
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to list character hair but is not a game master.`)
return callback([])
}
// get all objects
const items = await characterHairRepository.getAll()
callback(items)
}
}

View File

@ -0,0 +1,43 @@
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterGender } from '#application/enums'
import { UUID } from '#application/types'
import CharacterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository'
type Payload = {
id: number
name: string
gender: CharacterGender
isSelectable: boolean
spriteId: UUID
}
export default class CharacterHairUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
const sprite = await SpriteRepository.getById(data.spriteId)
const characterHair = await CharacterHairRepository.getById(data.id)
if (characterHair) {
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update()
}
return callback(true)
} catch (error) {
this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)
return callback(false)
}
}
}

View File

@ -0,0 +1,41 @@
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
) {}
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<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const newCharacterType = await prisma.characterType.create({
data: {
name: 'New character type',
gender: CharacterGender.MALE,
race: CharacterRace.HUMAN
}
})
callback(true, newCharacterType)
} catch (error) {
console.error('Error creating character type:', error)
callback(false)
}
}
}

View File

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

View File

@ -0,0 +1,37 @@
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'
interface IPayload {}
export default class CharacterTypeListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) {
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
return callback([])
}
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`)
return callback([])
}
// get all objects
const items = await CharacterTypeRepository.getAll()
callback(items)
}
}

View File

@ -0,0 +1,53 @@
import { CharacterGender, CharacterRace } from '@prisma/client'
import { Server } from 'socket.io'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: number
name: string
gender: CharacterGender
race: CharacterRace
isSelectable: boolean
spriteId: string
}
export default class CharacterTypeUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:characterType:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
await prisma.characterType.update({
where: { id: data.id },
data: {
name: data.name,
gender: data.gender,
race: data.race,
isSelectable: data.isSelectable,
spriteId: data.spriteId
}
})
callback(true)
} catch (error) {
console.error(error)
callback(false)
}
}
}

View File

@ -0,0 +1,42 @@
import { Server } from 'socket.io'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
export default class ItemCreateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:item:create', this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const newItem = await prisma.item.create({
data: {
name: 'New Item',
itemType: 'WEAPON',
stackable: false,
rarity: 'COMMON',
spriteId: null
}
})
callback(true, newItem)
} catch (error) {
console.error('Error creating item:', error)
callback(false)
}
}
}

View File

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

View File

@ -0,0 +1,37 @@
import { Item } from '@prisma/client'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import itemRepository from '#repositories/itemRepository'
interface IPayload {}
export default class ItemListEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:item:list', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) {
gameMasterLogger.error('gm:item:list error', 'Character not found')
return callback([])
}
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to list items but is not a game master.`)
return callback([])
}
// get all items
const items = await itemRepository.getAll()
callback(items)
}
}

View File

@ -0,0 +1,56 @@
import { ItemType, ItemRarity } from '@prisma/client'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: string
name: string
description: string | null
itemType: ItemType
stackable: boolean
rarity: ItemRarity
spriteId: string | null
}
export default class ItemUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:item:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
await prisma.item.update({
where: { id: data.id },
data: {
name: data.name,
description: data.description,
itemType: data.itemType,
stackable: data.stackable,
rarity: data.rarity,
spriteId: data.spriteId
}
})
return callback(true)
} catch (error) {
gameMasterLogger.error(`Error updating item: ${error instanceof Error ? error.message : String(error)}`)
return callback(false)
}
}
}

View File

@ -1,9 +1,9 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import { Object } from '@prisma/client'
import ObjectRepository from '../../../../repositories/objectRepository'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
import { Server } from 'socket.io'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import ObjectRepository from '#repositories/objectRepository'
interface IPayload {}
@ -14,10 +14,10 @@ export default class ObjectListEvent {
) {}
public listen(): void {
this.socket.on('gm:object:list', this.handleObjectList.bind(this))
this.socket.on('gm:object:list', this.handleEvent.bind(this))
}
private async handleObjectList(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback([])
@ -29,4 +29,4 @@ export default class ObjectListEvent {
const objects = await ObjectRepository.getAll()
callback(objects)
}
}
}

View File

@ -1,10 +1,12 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import path from 'path'
import fs from 'fs'
import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
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
@ -17,10 +19,10 @@ export default class ObjectRemoveEvent {
) {}
public listen(): void {
this.socket.on('gm:object:remove', this.handleObjectRemove.bind(this))
this.socket.on('gm:object:remove', this.handleEvent.bind(this))
}
private async handleObjectRemove(data: IPayload, callback: (response: boolean) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
@ -36,22 +38,22 @@ export default class ObjectRemoveEvent {
})
// get root path
const public_folder = path.join(process.cwd(), 'public', 'objects')
const public_folder = Storage.getPublicPath('objects')
// remove the tile from the disk
const finalFilePath = path.join(public_folder, data.object + '.png')
const finalFilePath = Storage.getPublicPath('objects', data.object + '.png')
fs.unlink(finalFilePath, (err) => {
if (err) {
console.log(err)
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
callback(false)
return
}
callback(true)
})
} catch (e) {
console.log(e)
} catch (error) {
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
}
}
}
}

View File

@ -1,8 +1,8 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: string
@ -11,7 +11,7 @@ type Payload = {
originX: number
originY: number
isAnimated: boolean
frameSpeed: number
frameRate: number
frameWidth: number
frameHeight: number
}
@ -23,10 +23,10 @@ export default class ObjectUpdateEvent {
) {}
public listen(): void {
this.socket.on('gm:object:update', this.handleObjectUpdate.bind(this))
this.socket.on('gm:object:update', this.handleEvent.bind(this))
}
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
@ -45,7 +45,7 @@ export default class ObjectUpdateEvent {
originX: data.originX,
originY: data.originY,
isAnimated: data.isAnimated,
frameSpeed: data.frameSpeed,
frameRate: data.frameRate,
frameWidth: data.frameWidth,
frameHeight: data.frameHeight
}
@ -56,4 +56,4 @@ export default class ObjectUpdateEvent {
callback(false)
}
}
}
}

View File

@ -1,12 +1,14 @@
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 prisma from '../../../../utilities/prisma'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
interface IObjectData {
[key: string]: Buffer
@ -19,10 +21,10 @@ export default class ObjectUploadEvent {
) {}
public listen(): void {
this.socket.on('gm:object:upload', this.handleObjectUpload.bind(this))
this.socket.on('gm:object:upload', this.handleEvent.bind(this))
}
private async handleObjectUpload(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
@ -30,7 +32,7 @@ export default class ObjectUploadEvent {
if (character.role !== 'gm') {
return callback(false)
}
const public_folder = path.join(process.cwd(), 'public', 'objects')
const public_folder = Storage.getPublicPath('objects')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
@ -54,7 +56,7 @@ export default class ObjectUploadEvent {
const uuid = object.id
const filename = `${uuid}.png`
const finalFilePath = path.join(public_folder, filename)
const finalFilePath = Storage.getPublicPath('objects', filename)
await writeFile(finalFilePath, objectData)
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)

View File

@ -0,0 +1,79 @@
import { Server } from 'socket.io'
import type { Prisma } from '@prisma/client'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
interface CopyPayload {
id: string
}
export default class SpriteCopyEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:sprite:copy', this.handleEvent.bind(this))
}
private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.validateGameMasterAccess())) {
return callback(false)
}
const sourceSprite = await prisma.sprite.findUnique({
where: { id: payload.id },
include: {
spriteActions: true
}
})
if (!sourceSprite) {
throw new Error('Source sprite not found')
}
const newSprite = await prisma.sprite.create({
data: {
name: `${sourceSprite.name} (Copy)`,
spriteActions: {
create: sourceSprite.spriteActions.map((action) => ({
action: action.action,
sprites: action.sprites as Prisma.InputJsonValue,
originX: action.originX,
originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameWidth: action.frameWidth,
frameHeight: action.frameHeight,
frameRate: action.frameRate
}))
}
}
})
callback(true)
} catch (error) {
this.handleError(error, payload.id, callback)
}
}
private async validateGameMasterAccess(): Promise<boolean> {
const character = await CharacterRepository.getById(this.socket.characterId!)
return character?.role === 'gm'
}
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
gameMasterLogger.error(`Error copying sprite ${spriteId}: ${this.getErrorMessage(error)}`)
callback(false)
}
private getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
}

View File

@ -1,10 +1,11 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import path from 'path'
import fs from 'fs/promises'
import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
import { Server } from 'socket.io'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
export default class SpriteCreateEvent {
constructor(
@ -13,19 +14,19 @@ export default class SpriteCreateEvent {
) {}
public listen(): void {
this.socket.on('gm:sprite:create', this.handleSpriteCreate.bind(this))
this.socket.on('gm:sprite:create', this.handleEvent.bind(this))
}
private async handleSpriteCreate(data: undefined, callback: (response: boolean) => void): Promise<void> {
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const public_folder = path.join(process.cwd(), 'public', 'sprites')
const public_folder = Storage.getPublicPath('sprites')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
@ -38,7 +39,7 @@ export default class SpriteCreateEvent {
const uuid = sprite.id
// Create folder with uuid
const sprite_folder = path.join(public_folder, uuid)
const sprite_folder = Storage.getPublicPath('sprites', uuid)
await fs.mkdir(sprite_folder, { recursive: true })
callback(true)
@ -47,4 +48,4 @@ export default class SpriteCreateEvent {
callback(false)
}
}
}
}

View File

@ -1,10 +1,12 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import fs from 'fs'
import path from 'path'
import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import { gameMasterLogger } from '../../../../utilities/logger'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
type Payload = {
id: string
@ -17,15 +19,15 @@ export default class GMSpriteDeleteEvent {
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = path.join(process.cwd(), 'public', 'sprites')
this.public_folder = Storage.getPublicPath('sprites')
}
public listen(): void {
this.socket.on('gm:sprite:delete', this.handleSpriteDelete.bind(this))
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
}
private async handleSpriteDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = CharacterManager.getCharacterFromSocket(this.socket)
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = await CharacterRepository.getById(this.socket.characterId!)
if (character?.role !== 'gm') {
return callback(false)
}
@ -43,7 +45,7 @@ export default class GMSpriteDeleteEvent {
}
private async deleteSpriteFolder(spriteId: string): Promise<void> {
const finalFilePath = path.join(this.public_folder, spriteId)
const finalFilePath = Storage.getPublicPath('sprites', spriteId)
if (fs.existsSync(finalFilePath)) {
await fs.promises.rmdir(finalFilePath, { recursive: true })

View File

@ -1,9 +1,9 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import { Sprite } from '@prisma/client'
import SpriteRepository from '../../../../repositories/spriteRepository'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
import { Server } from 'socket.io'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository'
interface IPayload {}
@ -14,11 +14,11 @@ export default class SpriteListEvent {
) {}
public listen(): void {
this.socket.on('gm:sprite:list', this.handleSpriteList.bind(this))
this.socket.on('gm:sprite:list', this.handleEvent.bind(this))
}
private async handleSpriteList(data: any, callback: (response: Sprite[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
private async handleEvent(data: any, callback: (response: Sprite[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback([])
if (character.role !== 'gm') {
@ -29,4 +29,4 @@ export default class SpriteListEvent {
const sprites = await SpriteRepository.getAll()
callback(sprites)
}
}
}

View File

@ -0,0 +1,402 @@
import { writeFile, mkdir } from 'node:fs/promises'
import sharp from 'sharp'
import { Server } from 'socket.io'
import type { Prisma, SpriteAction } from '@prisma/client'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
// Constants
const ISOMETRIC_CONFIG = {
tileWidth: 64,
tileHeight: 32,
centerOffset: 32,
bodyRatios: {
topStart: 0.15,
topEnd: 0.45,
weightUpper: 0.7,
weightLower: 0.3
}
} as const
// Types
interface ContentBounds {
left: number
right: number
top: number
bottom: number
width: number
height: number
}
interface SpriteActionInput extends Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> {
sprites: string[]
}
interface UpdatePayload {
id: string
name: string
spriteActions: Prisma.JsonValue
}
interface ProcessedSpriteAction extends SpriteActionInput {
frameWidth: number
frameHeight: number
buffersWithDimensions: ProcessedFrame[]
}
interface ProcessedFrame {
buffer: Buffer
width: number
height: number
}
interface SpriteAnalysis {
massCenter: number
spinePosition: number
contentBounds: ContentBounds
}
export default class SpriteUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:sprite:update', this.handleEvent.bind(this))
}
private async handleEvent(payload: UpdatePayload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.validateGameMasterAccess())) {
return callback(false)
}
const parsedActions = this.validateSpriteActions(payload.spriteActions)
// Process sprites
const processedActions = await Promise.all(
parsedActions.map(async (action) => {
const spriteBuffers = await this.convertBase64ToBuffers(action.sprites)
const frameWidth = ISOMETRIC_CONFIG.tileWidth
const frameHeight = await this.calculateOptimalHeight(spriteBuffers)
const processedFrames = await this.normalizeFrames(spriteBuffers, frameWidth, frameHeight)
return {
...action,
frameWidth,
frameHeight,
buffersWithDimensions: processedFrames
}
})
)
await Promise.all([
this.updateDatabase(payload.id, payload.name, processedActions),
this.saveSpritesToDisk(
payload.id,
processedActions.filter((a) => a.buffersWithDimensions.length > 0)
)
])
callback(true)
} catch (error) {
this.handleError(error, payload.id, callback)
}
}
private async validateGameMasterAccess(): Promise<boolean> {
const character = await CharacterRepository.getById(this.socket.characterId!)
return character?.role === 'gm'
}
private validateSpriteActions(actions: Prisma.JsonValue): SpriteActionInput[] {
try {
const parsed = JSON.parse(JSON.stringify(actions)) as SpriteActionInput[]
if (!Array.isArray(parsed)) {
throw new Error('Sprite actions must be an array')
}
return parsed
} catch (error) {
throw new Error(`Invalid sprite actions format: ${this.getErrorMessage(error)}`)
}
}
private async convertBase64ToBuffers(sprites: string[]): Promise<Buffer[]> {
return sprites.map((sprite) => Buffer.from(sprite.split(',')[1], 'base64'))
}
private async normalizeFrames(buffers: Buffer[], frameWidth: number, frameHeight: number): Promise<ProcessedFrame[]> {
return Promise.all(
buffers.map(async (buffer) => {
const normalizedBuffer = await this.normalizeIsometricSprite(buffer, frameWidth, frameHeight)
return {
buffer: normalizedBuffer,
width: frameWidth,
height: frameHeight
}
})
)
}
private async calculateOptimalHeight(buffers: Buffer[]): Promise<number> {
if (!buffers.length) return ISOMETRIC_CONFIG.tileHeight // Return default height if no buffers
const heights = await Promise.all(
buffers.map(async (buffer) => {
const bounds = await this.findContentBounds(buffer)
return bounds.height
})
)
return Math.ceil(Math.max(...heights) / 2) * 2
}
private async normalizeIsometricSprite(buffer: Buffer, frameWidth: number, frameHeight: number): Promise<Buffer> {
const analysis = await this.analyzeIsometricSprite(buffer)
const idealCenter = Math.floor(frameWidth / 2)
const offset = Math.round(idealCenter - analysis.massCenter)
// Process the input sprite
const processedInput = await sharp(buffer)
.ensureAlpha()
.resize({
width: frameWidth, // Set maximum width
height: frameHeight, // Set maximum height
fit: 'inside', // Ensure image fits within dimensions
kernel: sharp.kernel.nearest,
position: 'center',
withoutEnlargement: true // Don't enlarge smaller images
})
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256
})
.toBuffer()
// Create the final composition
return sharp({
create: {
width: frameWidth,
height: frameHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite([
{
input: processedInput,
left: offset,
top: 0,
blend: 'over'
}
])
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256
})
.toBuffer()
}
private async analyzeIsometricSprite(buffer: Buffer): Promise<SpriteAnalysis> {
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
const { width, height } = info
const upperStart = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topStart)
const upperEnd = Math.floor(height * ISOMETRIC_CONFIG.bodyRatios.topEnd)
const { columnDensity, upperBodyDensity, bounds } = this.calculatePixelDistribution(data, width, height, upperStart, upperEnd)
const spinePosition = this.findSpinePosition(upperBodyDensity)
const massCenter = this.calculateWeightedMassCenter(columnDensity, upperBodyDensity)
return {
massCenter,
spinePosition,
contentBounds: bounds
}
}
private calculatePixelDistribution(data: Buffer, width: number, height: number, upperStart: number, upperEnd: number) {
const columnDensity = new Array(width).fill(0)
const upperBodyDensity = new Array(width).fill(0)
const bounds = { left: width, right: 0, top: height, bottom: 0 }
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (data[(y * width + x) * 4 + 3] > 0) {
columnDensity[x]++
if (y >= upperStart && y <= upperEnd) {
upperBodyDensity[x]++
}
this.updateBounds(bounds, x, y)
}
}
}
return {
columnDensity,
upperBodyDensity,
bounds: {
...bounds,
width: bounds.right - bounds.left + 1,
height: bounds.bottom - bounds.top + 1
}
}
}
private updateBounds(bounds: { left: number; right: number; top: number; bottom: number }, x: number, y: number): void {
bounds.left = Math.min(bounds.left, x)
bounds.right = Math.max(bounds.right, x)
bounds.top = Math.min(bounds.top, y)
bounds.bottom = Math.max(bounds.bottom, y)
}
private findSpinePosition(density: number[]): number {
return density.reduce((maxIdx, curr, idx, arr) => (curr > arr[maxIdx] ? idx : maxIdx), 0)
}
private calculateWeightedMassCenter(columnDensity: number[], upperBodyDensity: number[]): number {
const upperMassCenter = this.calculateMassCenter(upperBodyDensity)
const lowerMassCenter = this.calculateMassCenter(columnDensity)
return Math.round(upperMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightUpper + lowerMassCenter * ISOMETRIC_CONFIG.bodyRatios.weightLower)
}
private calculateMassCenter(density: number[]): number {
const totalMass = density.reduce((sum, mass) => sum + mass, 0)
if (!totalMass) return 0
const weightedSum = density.reduce((sum, mass, position) => sum + position * mass, 0)
return Math.round(weightedSum / totalMass)
}
private async findContentBounds(buffer: Buffer) {
const { data, info } = await sharp(buffer).raw().ensureAlpha().toBuffer({ resolveWithObject: true })
const width = info.width
const height = info.height
let left = width
let right = 0
let top = height
let bottom = 0
// Find actual content boundaries by checking alpha channel
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4
if (data[idx + 3] > 0) {
// If pixel is not transparent
left = Math.min(left, x)
right = Math.max(right, x)
top = Math.min(top, y)
bottom = Math.max(bottom, y)
}
}
}
return {
width: right - left + 1,
height: bottom - top + 1,
leftOffset: left,
topOffset: top
}
}
private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> {
const publicFolder = Storage.getPublicPath('sprites', id)
await mkdir(publicFolder, { recursive: true })
await Promise.all(
actions.map(async (action) => {
const spritesheet = await this.createSpritesheet(action.buffersWithDimensions)
await writeFile(Storage.getPublicPath('sprites', id, `${action.action}.png`), spritesheet)
})
)
}
private async createSpritesheet(frames: ProcessedFrame[]): Promise<Buffer> {
const background = await sharp({
create: {
width: ISOMETRIC_CONFIG.tileWidth * frames.length,
height: frames[0].height,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256,
dither: 0
})
.toBuffer()
return sharp(background)
.composite(
frames.map((frame, index) => ({
input: frame.buffer,
left: index * ISOMETRIC_CONFIG.tileWidth,
top: 0,
blend: 'over'
}))
)
.png({
compressionLevel: 9,
adaptiveFiltering: false,
palette: true,
quality: 100,
colors: 256,
dither: 0
})
.toBuffer()
}
private async updateDatabase(id: string, name: string, actions: ProcessedSpriteAction[]): Promise<void> {
await prisma.sprite.update({
where: { id },
data: {
name,
spriteActions: {
deleteMany: { spriteId: id },
create: actions.map(this.mapActionToDatabase)
}
}
})
}
private mapActionToDatabase(action: ProcessedSpriteAction) {
return {
action: action.action,
sprites: action.sprites,
originX: action.originX,
originY: action.originY,
isAnimated: action.isAnimated,
isLooping: action.isLooping,
frameWidth: action.frameWidth,
frameHeight: action.frameHeight,
frameRate: action.frameRate
}
}
private handleError(error: unknown, spriteId: string, callback: (success: boolean) => void): void {
gameMasterLogger.error(`Error updating sprite ${spriteId}: ${this.getErrorMessage(error)}`)
callback(false)
}
private getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
}

View File

@ -1,10 +1,12 @@
import path from 'path'
import fs from 'fs/promises'
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: string
@ -17,14 +19,14 @@ export default class GMTileDeleteEvent {
private readonly io: Server,
private readonly socket: TSocket
) {
this.public_folder = path.join(process.cwd(), 'public', 'tiles')
this.public_folder = Storage.getPublicPath('tiles')
}
public listen(): void {
this.socket.on('gm:tile:delete', this.handleTileDelete.bind(this))
this.socket.on('gm:tile:delete', this.handleEvent.bind(this))
}
private async handleTileDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
@ -54,7 +56,7 @@ export default class GMTileDeleteEvent {
}
private async deleteTileFile(tileId: string): Promise<void> {
const finalFilePath = path.join(this.public_folder, `${tileId}.png`)
const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`)
try {
await fs.unlink(finalFilePath)
} catch (error: any) {

View File

@ -1,9 +1,9 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import { Tile } from '@prisma/client'
import TileRepository from '../../../../repositories/tileRepository'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
import { Server } from 'socket.io'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import TileRepository from '#repositories/tileRepository'
interface IPayload {}
@ -14,10 +14,10 @@ export default class TileListEvent {
) {}
public listen(): void {
this.socket.on('gm:tile:list', this.handleTileList.bind(this))
this.socket.on('gm:tile:list', this.handleEvent.bind(this))
}
private async handleTileList(data: any, callback: (response: Tile[]) => void): Promise<void> {
private async handleEvent(data: any, callback: (response: Tile[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return
@ -29,4 +29,4 @@ export default class TileListEvent {
const tiles = await TileRepository.getAll()
callback(tiles)
}
}
}

View File

@ -1,8 +1,8 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../../utilities/types'
import prisma from '../../../../utilities/prisma'
import CharacterManager from '../../../../managers/characterManager'
import characterRepository from '../../../../repositories/characterRepository'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: string
@ -17,10 +17,10 @@ export default class TileUpdateEvent {
) {}
public listen(): void {
this.socket.on('gm:tile:update', this.handleTileUpdate.bind(this))
this.socket.on('gm:tile:update', this.handleEvent.bind(this))
}
private async handleTileUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
@ -45,4 +45,4 @@ export default class TileUpdateEvent {
callback(false)
}
}
}
}

View File

@ -1,11 +1,13 @@
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 prisma from '../../../../utilities/prisma'
import characterRepository from '../../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../../utilities/logger'
import { writeFile } from 'node:fs/promises'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
interface ITileData {
[key: string]: Buffer
@ -18,10 +20,10 @@ export default class TileUploadEvent {
) {}
public listen(): void {
this.socket.on('gm:tile:upload', this.handleTileUpload.bind(this))
this.socket.on('gm:tile:upload', this.handleEvent.bind(this))
}
private async handleTileUpload(data: ITileData, callback: (response: boolean) => void): Promise<void> {
private async handleEvent(data: ITileData, callback: (response: boolean) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
@ -30,7 +32,7 @@ export default class TileUploadEvent {
return
}
const public_folder = path.join(process.cwd(), 'public', 'tiles')
const public_folder = Storage.getPublicPath('tiles')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
@ -43,7 +45,7 @@ export default class TileUploadEvent {
})
const uuid = tile.id
const filename = `${uuid}.png`
const finalFilePath = path.join(public_folder, filename)
const finalFilePath = Storage.getPublicPath('tiles', filename)
await writeFile(finalFilePath, tileData)
})
@ -55,4 +57,4 @@ export default class TileUploadEvent {
callback(false)
}
}
}
}

View File

@ -1,10 +1,11 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../utilities/types'
import ZoneRepository from '../../../repositories/zoneRepository'
import { Zone } from '@prisma/client'
import prisma from '../../../utilities/prisma'
import CharacterRepository from '../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../utilities/logger'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository'
type Payload = {
name: string
@ -19,10 +20,10 @@ export default class ZoneCreateEvent {
) {}
public listen(): void {
this.socket.on('gm:zone_editor:zone:create', this.handleZoneCreate.bind(this))
this.socket.on('gm:zone_editor:zone:create', this.handleEvent.bind(this))
}
private async handleZoneCreate(data: Payload, callback: (response: Zone[]) => void): Promise<void> {
private async handleEvent(data: Payload, callback: (response: Zone[]) => void): Promise<void> {
try {
const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) {
@ -59,4 +60,4 @@ export default class ZoneCreateEvent {
callback([])
}
}
}
}

View File

@ -1,9 +1,10 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../utilities/types'
import ZoneRepository from '../../../repositories/zoneRepository'
import prisma from '../../../utilities/prisma'
import CharacterRepository from '../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../utilities/logger'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository'
type Payload = {
zoneId: number
@ -16,10 +17,10 @@ export default class ZoneDeleteEvent {
) {}
public listen(): void {
this.socket.on('gm:zone_editor:zone:delete', this.handleZoneDelete.bind(this))
this.socket.on('gm:zone_editor:zone:delete', this.handleEvent.bind(this))
}
private async handleZoneDelete(data: Payload, callback: (response: boolean) => void): Promise<void> {
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
try {
const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) {
@ -58,4 +59,4 @@ export default class ZoneDeleteEvent {
callback(false)
}
}
}
}

View File

@ -1,9 +1,10 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../utilities/types'
import { Zone } from '@prisma/client'
import ZoneRepository from '../../../repositories/zoneRepository'
import CharacterRepository from '../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../utilities/logger'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {}
@ -14,10 +15,10 @@ export default class ZoneListEvent {
) {}
public listen(): void {
this.socket.on('gm:zone_editor:zone:list', this.handleZoneList.bind(this))
this.socket.on('gm:zone_editor:zone:list', this.handleEvent.bind(this))
}
private async handleZoneList(data: IPayload, callback: (response: Zone[]) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: Zone[]) => void): Promise<void> {
try {
const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) {
@ -41,4 +42,4 @@ export default class ZoneListEvent {
callback([])
}
}
}
}

View File

@ -1,9 +1,10 @@
import { Server } from 'socket.io'
import { TSocket } from '../../../utilities/types'
import ZoneRepository from '../../../repositories/zoneRepository'
import { Zone } from '@prisma/client'
import CharacterRepository from '../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../utilities/logger'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {
zoneId: number
@ -16,10 +17,10 @@ export default class ZoneRequestEvent {
) {}
public listen(): void {
this.socket.on('gm:zone_editor:zone:request', this.handleZoneRequest.bind(this))
this.socket.on('gm:zone_editor:zone:request', this.handleEvent.bind(this))
}
private async handleZoneRequest(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try {
const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) {
@ -56,4 +57,4 @@ export default class ZoneRequestEvent {
callback(null)
}
}
}
}

View File

@ -1,11 +1,12 @@
import { Zone, ZoneEffect, ZoneEventTileType, ZoneObject } from '@prisma/client'
import { Server } from 'socket.io'
import { TSocket } from '../../../utilities/types'
import ZoneRepository from '../../../repositories/zoneRepository'
import { Zone, ZoneEventTileType, ZoneObject } from '@prisma/client'
import prisma from '../../../utilities/prisma'
import zoneManager from '../../../managers/zoneManager'
import CharacterRepository from '../../../repositories/characterRepository'
import { gameMasterLogger } from '../../../utilities/logger'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import zoneManager from '#managers/zoneManager'
import CharacterRepository from '#repositories/characterRepository'
import ZoneRepository from '#repositories/zoneRepository'
interface IPayload {
zoneId: number
@ -22,8 +23,13 @@ interface IPayload {
toZoneId: number
toPositionX: number
toPositionY: number
toRotation: number
}
}[]
zoneEffects: {
effect: string
strength: number
}[]
zoneObjects: ZoneObject[]
}
@ -34,40 +40,52 @@ export default class ZoneUpdateEvent {
) {}
public listen(): void {
this.socket.on('gm:zone_editor:zone:update', this.handleZoneUpdate.bind(this))
this.socket.on('gm:zone_editor:zone:update', this.handleEvent.bind(this))
}
private async handleZoneUpdate(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
private async handleEvent(data: IPayload, callback: (response: Zone | null) => void): Promise<void> {
try {
const character = await CharacterRepository.getById(this.socket.characterId as number)
if (!character) {
gameMasterLogger.error('gm:zone_editor:zone:update error', 'Character not found')
callback(null)
return
return callback(null)
}
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to update zone but is not a game master.`)
callback(null)
return
return callback(null)
}
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
if (!data.zoneId) {
gameMasterLogger.info(`User ${character.id} tried to update zone but did not provide a zone id.`)
callback(null)
return
return callback(null)
}
let zone = await ZoneRepository.getById(data.zoneId)
if (!zone) {
gameMasterLogger.info(`User ${character.id} tried to update zone ${data.zoneId} but it does not exist.`)
callback(null)
return
return callback(null)
}
// If tiles are larger than the zone, remove the extra tiles
if (data.tiles.length > data.height) {
data.tiles = data.tiles.slice(0, data.height)
}
for (let i = 0; i < data.tiles.length; i++) {
if (data.tiles[i].length > data.width) {
data.tiles[i] = data.tiles[i].slice(0, data.width)
}
}
// If zone event tiles are placed outside the zone's bounds, remove these
data.zoneEventTiles = data.zoneEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
// If zone objects are placed outside the zone's bounds, remove these
data.zoneObjects = data.zoneObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
await prisma.zone.update({
where: { id: data.zoneId },
data: {
@ -84,14 +102,15 @@ export default class ZoneUpdateEvent {
positionY: zoneEventTile.positionY,
...(zoneEventTile.type === 'TELEPORT' && zoneEventTile.teleport
? {
teleport: {
create: {
toZoneId: zoneEventTile.teleport.toZoneId,
toPositionX: zoneEventTile.teleport.toPositionX,
toPositionY: zoneEventTile.teleport.toPositionY
teleport: {
create: {
toZoneId: zoneEventTile.teleport.toZoneId,
toPositionX: zoneEventTile.teleport.toPositionX,
toPositionY: zoneEventTile.teleport.toPositionY,
toRotation: zoneEventTile.teleport.toRotation
}
}
}
}
: {})
}))
},
@ -100,10 +119,19 @@ export default class ZoneUpdateEvent {
create: data.zoneObjects.map((zoneObject) => ({
objectId: zoneObject.objectId,
depth: zoneObject.depth,
isRotated: zoneObject.isRotated,
positionX: zoneObject.positionX,
positionY: zoneObject.positionY
}))
}
},
zoneEffects: {
deleteMany: { zoneId: data.zoneId },
create: data.zoneEffects.map((zoneEffect) => ({
effect: zoneEffect.effect,
strength: zoneEffect.strength
}))
},
updatedAt: new Date()
}
})
@ -115,13 +143,18 @@ export default class ZoneUpdateEvent {
return
}
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
callback(zone)
/**
* @TODO #246: Reload zone for players who are currently in the zone
*/
zoneManager.unloadZone(data.zoneId)
await zoneManager.loadZone(zone)
} catch (error: any) {
gameMasterLogger.error('gm:zone_editor:zone:update error', error.message)
gameMasterLogger.error(`gm:zone_editor:zone:update error: ${error instanceof Error ? error.message : String(error)}`)
callback(null)
}
}
}
}

22
src/events/login.ts Normal file
View File

@ -0,0 +1,22 @@
import { BaseEvent } from '#application/base/baseEvent'
import UserRepository from '#repositories/userRepository'
export default class LoginEvent extends BaseEvent {
public listen(): void {
this.socket.on('login', this.handleEvent.bind(this))
}
private handleEvent(): void {
try {
if (!this.socket.userId) {
this.logger.warn('Login attempt without user data')
return
}
this.socket.emit('logged_in', { user: UserRepository.getById(this.socket.userId) })
this.logger.info(`User logged in: ${this.socket.userId}`)
} catch (error: any) {
this.logger.error('login error: ' + error.message)
}
}
}

View File

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

View File

@ -0,0 +1,17 @@
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
export default class Weather extends BaseEvent {
public listen(): void {
this.socket.on('weather', this.handleEvent.bind(this))
}
private async handleEvent(): Promise<void> {
try {
const weather = WeatherManager.getWeatherState()
this.socket.emit('weather', weather)
} catch (error: any) {
this.logger.error('weather error: ' + error.message)
}
}
}

View File

@ -0,0 +1,115 @@
import fs from 'fs'
import { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import Database from '#application/database'
import Storage from '#application/storage'
import { AssetData, UUID } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository'
import TileRepository from '#repositories/tileRepository'
import ZoneRepository from '#repositories/zoneRepository'
export class AssetsController extends BaseController {
/**
* List tiles
* @param req
* @param res
*/
public async listTiles(req: Request, res: Response) {
const assets: AssetData[] = []
const tiles = await TileRepository.getAll()
for (const tile of tiles) {
assets.push({ key: tile.getId(), data: '/assets/tiles/' + tile.getId() + '.png', group: 'tiles', updatedAt: tile.getUpdatedAt() } as AssetData)
}
return this.sendSuccess(res, assets)
}
/**
* List tiles by zone
* @param req
* @param res
*/
public async listTilesByZone(req: Request, res: Response) {
const zoneId = req.params.zoneId as UUID
if (!zoneId) {
return this.sendError(res, 'Invalid zone ID', 400)
}
const zone = await ZoneRepository.getById(zoneId)
if (!zone) {
return this.sendError(res, 'Zone not found', 404)
}
const assets: AssetData[] = []
const tiles = await TileRepository.getByZoneId(zoneId)
for (const tile of tiles) {
assets.push({ key: tile.getId(), data: '/assets/tiles/' + tile.getId() + '.png', group: 'tiles', updatedAt: tile.getUpdatedAt() } as AssetData)
}
return this.sendSuccess(res, assets)
}
/**
* List sprite actions
* @param req
* @param res
*/
public async listSpriteActions(req: Request, res: Response) {
const spriteId = req.params.spriteId as UUID
if (!spriteId) {
return this.sendError(res, 'Invalid sprite ID', 400)
}
const sprite = await SpriteRepository.getById(spriteId)
if (!sprite) {
return this.sendError(res, 'Sprite not found', 404)
}
await Database.getEntityManager().populate(sprite, ['spriteActions'])
const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({
key: sprite.id + '-' + spriteAction.action,
data: '/assets/sprites/' + sprite.getId() + '/' + spriteAction.getAction() + '.png',
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
updatedAt: sprite.getUpdatedAt(),
originX: Number(spriteAction.originX.toString()),
originY: Number(spriteAction.originY.toString()),
isAnimated: spriteAction.getIsAnimated(),
frameRate: spriteAction.getFrameRate(),
frameWidth: spriteAction.getFrameWidth(),
frameHeight: spriteAction.getFrameHeight(),
frameCount: JSON.parse(JSON.stringify(spriteAction.getSprites())).length
}))
return this.sendSuccess(res, assets)
}
/**
* Download asset
* @param req
* @param res
*/
public async downloadAsset(req: Request, res: Response) {
const { type, spriteId, file } = req.params
const assetPath = type === 'sprites' && spriteId ? Storage.getPublicPath(type, spriteId, file) : Storage.getPublicPath(type, file)
if (!fs.existsSync(assetPath)) {
this.logger.error(`File not found: ${assetPath}`)
return this.sendError(res, 'Asset not found', 404)
}
res.sendFile(assetPath, (err) => {
if (err) {
this.logger.error('Error sending file:' + err)
this.sendError(res, 'Error downloading the asset', 500)
}
})
}
}

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