From 63bc53413fd5fa17ff86de57db941e208b66de1b Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:55:36 -0400 Subject: [PATCH 01/20] docs: make Dockerfile link relative --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cb1ebcd..e8ea8732 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Live versions: Run the project with either Deno 2 or Docker. - Deno 2: https://deno.com/ -- Dockerfile: https://github.com/candirugame/candiru/blob/dev/Dockerfile +- Dockerfile: [candiru/Dockerfile](Dockerfile) ## Running with Deno 2 From 3153873366ae668eaf3f9634d18e618a2dfd2994 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:42:37 -0400 Subject: [PATCH 02/20] feat: add server-side prop physics --- .gitignore | 3 +- deno.json | 1 + deno.lock | 37 +- src/client/core/Networking.ts | 2 +- src/client/core/RemoteItemRenderer.ts | 5 +- src/client/core/RemotePlayerRenderer.ts | 2 +- src/client/input/CollisionManager.ts | 6 +- src/client/items/ItemBase.ts | 5 +- src/server/DataValidator.ts | 1 + src/server/GameEngine.ts | 6 +- src/server/GameServer.ts | 4 +- src/server/config.ts | 70 ++-- src/server/gamemodes/KingOfTheHillGamemode.ts | 4 +- src/server/gamemodes/SoloCTFGamemode.ts | 2 +- src/server/managers/ChatManager.ts | 6 +- src/server/managers/ItemManager.ts | 9 +- src/server/managers/PropManager.ts | 22 +- src/server/physics/PhysicsEngine.ts | 324 ++++++++++++++++++ src/shared/Prop.ts | 2 + src/shared/Utils.ts | 2 +- 20 files changed, 457 insertions(+), 56 deletions(-) create mode 100644 src/server/physics/PhysicsEngine.ts diff --git a/.gitignore b/.gitignore index 8b532550..138c1c85 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,5 @@ dist .nx/workspace-data/ servers.txt -candiru \ No newline at end of file +candiru +*.mjs diff --git a/deno.json b/deno.json index 4f7d6118..e380f267 100644 --- a/deno.json +++ b/deno.json @@ -21,6 +21,7 @@ "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0", "@oak/oak": "jsr:@oak/oak@^17.1.3", "@std/http": "jsr:@std/http@^1.0.10", + "@std/path": "jsr:@std/path@^1.0.6", "vite": "npm:vite@^5.4.8" }, "lint": { diff --git a/deno.lock b/deno.lock index 6f918377..b046b18e 100644 --- a/deno.lock +++ b/deno.lock @@ -11,6 +11,7 @@ "jsr:@std/http@1": "1.0.12", "jsr:@std/media-types@1": "1.1.0", "jsr:@std/path@1": "1.0.8", + "jsr:@std/path@^1.0.6": "1.0.8", "npm:@analogjs/content@^1.10.1": "1.19.1_@angular+common@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+core@19.2.14__rxjs@7.8.2__zone.js@0.15.1_@angular+platform-browser@19.2.14__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+router@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1_@nx+devkit@21.3.7__nx@21.3.7_front-matter@4.0.2_marked@5.1.2_marked-gfm-heading-id@3.2.0__marked@5.1.2_marked-highlight@2.2.2__marked@5.1.2_marked-mangle@1.1.11__marked@5.1.2_prismjs@1.30.0_rxjs@7.8.2_zone.js@0.15.1_@angular+animations@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1", "npm:@analogjs/platform@^1.10.1": "1.19.1_@nx+angular@21.3.7__@angular-devkit+build-angular@19.2.15___@angular+compiler-cli@19.2.14____@angular+compiler@19.2.14____typescript@5.5.4___@angular+platform-server@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+compiler@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____@angular+platform-browser@19.2.14_____@angular+animations@19.2.14______@angular+common@19.2.14_______@angular+core@19.2.14________rxjs@7.8.2________zone.js@0.15.1_______rxjs@7.8.2_______zone.js@0.15.1______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1___tailwindcss@3.4.17____postcss@8.5.6___typescript@5.5.4___webpack@5.98.0___webpack-dev-server@5.2.2____webpack@5.98.0____@types+express@4.17.23___@angular+compiler@19.2.14___less@4.2.2___postcss@8.5.2___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___@angular+platform-browser@19.2.14____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.1___@babel+core@7.26.10___vite@5.4.19____less@4.2.2____sass@1.85.0____terser@5.39.0___sass@1.85.0___terser@5.39.0___rxjs@7.8.2___zone.js@0.15.1___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1__@angular-devkit+core@19.2.15___ajv@8.17.1__@angular-devkit+schematics@19.2.15__@angular+build@19.2.15___@angular+compiler@19.2.14___@angular+compiler-cli@19.2.14____@angular+compiler@19.2.14____typescript@5.5.4___@angular+platform-server@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+compiler@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____@angular+platform-browser@19.2.14_____@angular+animations@19.2.14______@angular+common@19.2.14_______@angular+core@19.2.14________rxjs@7.8.2________zone.js@0.15.1_______rxjs@7.8.2_______zone.js@0.15.1______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1___postcss@8.5.6___tailwindcss@3.4.17____postcss@8.5.6___typescript@5.5.4___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___@angular+platform-browser@19.2.14____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___@babel+core@7.26.10___vite@6.2.7____sass@1.85.0___sass@1.85.0___zone.js@0.15.1___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___less@4.2.2__@schematics+angular@19.2.15__rxjs@7.8.2__@angular+compiler-cli@19.2.14___@angular+compiler@19.2.14___typescript@5.5.4__@angular+platform-server@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+compiler@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___@angular+platform-browser@19.2.14____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1__tailwindcss@3.4.17___postcss@8.5.6__typescript@5.5.4__@angular+compiler@19.2.14__postcss@8.5.6__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__vite@5.4.19___less@4.2.2___sass@1.85.0___terser@5.39.0__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1_@nx+devkit@21.3.7__nx@21.3.7_@nx+vite@21.3.7__vite@5.4.19___less@4.2.2___sass@1.85.0___terser@5.39.0__vitest@2.1.9___jsdom@22.1.0___vite@5.4.19____less@4.2.2____sass@1.85.0____terser@5.39.0__typescript@5.5.4__jsdom@22.1.0_marked@5.1.2_marked-gfm-heading-id@3.2.0__marked@5.1.2_marked-highlight@2.2.2__marked@5.1.2_marked-mangle@1.1.11__marked@5.1.2_vite@5.4.19__less@4.2.2__sass@1.85.0__terser@5.39.0_@angular-devkit+build-angular@19.2.15__@angular+compiler-cli@19.2.14___@angular+compiler@19.2.14___typescript@5.5.4__@angular+platform-server@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+compiler@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___@angular+platform-browser@19.2.14____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1__tailwindcss@3.4.17___postcss@8.5.6__typescript@5.5.4__webpack@5.98.0__webpack-dev-server@5.2.2___webpack@5.98.0___@types+express@4.17.23__@angular+compiler@19.2.14__less@4.2.2__postcss@8.5.2__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.1__@babel+core@7.26.10__vite@5.4.19___less@4.2.2___sass@1.85.0___terser@5.39.0__sass@1.85.0__terser@5.39.0__rxjs@7.8.2__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1_@angular+build@19.2.15__@angular+compiler@19.2.14__@angular+compiler-cli@19.2.14___@angular+compiler@19.2.14___typescript@5.5.4__@angular+platform-server@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+compiler@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___@angular+platform-browser@19.2.14____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1__postcss@8.5.6__tailwindcss@3.4.17___postcss@8.5.6__typescript@5.5.4__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__@babel+core@7.26.10__vite@6.2.7___sass@1.85.0__sass@1.85.0__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__less@4.2.2_rxjs@7.8.2_vitest@2.1.9__jsdom@22.1.0__vite@5.4.19___less@4.2.2___sass@1.85.0___terser@5.39.0_@angular+compiler-cli@19.2.14__@angular+compiler@19.2.14__typescript@5.5.4_@angular+platform-server@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+compiler@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1_tailwindcss@3.4.17__postcss@8.5.6_typescript@5.5.4_@angular+compiler@19.2.14_postcss@8.5.6_jsdom@22.1.0_@angular+common@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+core@19.2.14__rxjs@7.8.2__zone.js@0.15.1_@angular+platform-browser@19.2.14__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_zone.js@0.15.1_@angular+animations@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@types+node@22.12.0", "npm:@analogjs/router@^1.10.1": "1.19.1_@analogjs+content@1.19.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+router@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___@angular+platform-browser@19.2.14____@angular+animations@19.2.14_____@angular+common@19.2.14______@angular+core@19.2.14_______rxjs@7.8.2_______zone.js@0.15.1______rxjs@7.8.2______zone.js@0.15.1_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1__@nx+devkit@21.3.7___nx@21.3.7__front-matter@4.0.2__marked@5.1.2__marked-gfm-heading-id@3.2.0___marked@5.1.2__marked-highlight@2.2.2___marked@5.1.2__marked-mangle@1.1.11___marked@5.1.2__prismjs@1.30.0__rxjs@7.8.2__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1_@angular+core@19.2.14__rxjs@7.8.2__zone.js@0.15.1_@angular+router@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__@angular+platform-browser@19.2.14___@angular+animations@19.2.14____@angular+common@19.2.14_____@angular+core@19.2.14______rxjs@7.8.2______zone.js@0.15.1_____rxjs@7.8.2_____zone.js@0.15.1____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1_@angular+common@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+platform-browser@19.2.14__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_front-matter@4.0.2_marked@5.1.2_marked-gfm-heading-id@3.2.0__marked@5.1.2_marked-highlight@2.2.2__marked@5.1.2_marked-mangle@1.1.11__marked@5.1.2_prismjs@1.30.0_rxjs@7.8.2_zone.js@0.15.1_@angular+animations@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1", @@ -30,13 +31,18 @@ "npm:@angular/platform-server@19": "19.2.14_@angular+common@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+compiler@19.2.14_@angular+core@19.2.14__rxjs@7.8.2__zone.js@0.15.1_@angular+platform-browser@19.2.14__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_rxjs@7.8.2_zone.js@0.15.1_@angular+animations@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1", "npm:@angular/router@19": "19.2.14_@angular+common@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+core@19.2.14__rxjs@7.8.2__zone.js@0.15.1_@angular+platform-browser@19.2.14__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_rxjs@7.8.2_zone.js@0.15.1_@angular+animations@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1", "npm:@deno/vite-plugin@1": "1.0.5_vite@5.4.19__less@4.2.2__sass@1.85.0__terser@5.39.0_@types+node@22.12.0", + "npm:@dimforge/rapier3d-compat@*": "0.16.2", + "npm:@gltf-transform/core@*": "4.1.3", + "npm:@gltf-transform/extensions@*": "4.1.3", "npm:@semantic-release/git@^10.0.1": "10.0.1_semantic-release@24.2.7__typescript@5.5.4__marked@15.0.12_typescript@5.5.4", "npm:@types/node@*": "22.12.0", "npm:@types/three@0.169": "0.169.0", "npm:autoprefixer@^10.4.14": "10.4.21_postcss@8.5.6", "npm:dot-prop@9": "9.0.0", + "npm:draco3dgltf@1.5.6": "1.5.6", "npm:fdir@^6.4.2": "6.4.6_picomatch@4.0.2", "npm:front-matter@^4.0.2": "4.0.2", + "npm:gl-matrix@*": "3.4.4", "npm:joi@^17.13.3": "17.13.3", "npm:jsdom@22": "22.1.0", "npm:marked-gfm-heading-id@^3.1.0": "3.2.0_marked@5.1.2", @@ -85,7 +91,7 @@ "jsr:@std/bytes", "jsr:@std/http", "jsr:@std/media-types", - "jsr:@std/path", + "jsr:@std/path@1", "npm:path-to-regexp" ] }, @@ -1702,6 +1708,9 @@ "node-source-walk" ] }, + "@dimforge/rapier3d-compat@0.16.2": { + "integrity": "sha512-VM4pOmZbWUgXzuSJbNmoiDVhurCV8KnHhg0/MB7cNcJEjkrhwTSlnMSAe7OCUU0MXnSJciXdIFOgJU8eCzGGkQ==" + }, "@discoveryjs/json-ext@0.6.3": { "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==" }, @@ -2276,6 +2285,19 @@ "@fastify/busboy@3.1.1": { "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==" }, + "@gltf-transform/core@4.1.3": { + "integrity": "sha512-N+73Vo9DTXV2QmsnetLRY4q3z0Q0oyH0i/ymvzEkgpgNEAq+RP73ZLY0HK+Ia0rTUMgFwQHFNyHDyFiENToBZA==", + "dependencies": [ + "property-graph" + ] + }, + "@gltf-transform/extensions@4.1.3": { + "integrity": "sha512-RcjA6UfBqOQPMqYhY/ftHAjrO1mnGLxUIXwErrH8qBoMprgkfLmi3fZuNL1tgZXlSPTjYdMCd7zEOc707F8Ekg==", + "dependencies": [ + "@gltf-transform/core", + "ktx-parse" + ] + }, "@hapi/hoek@9.3.0": { "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" }, @@ -7012,6 +7034,9 @@ "dotenv@16.6.1": { "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==" }, + "draco3dgltf@1.5.6": { + "integrity": "sha512-wCKFfJhRB2lkAV/dzlKs+EmHSoI3x8w4Z8yBpkzLIF9GAvjh6jk90jomC13k/DLVFTJSesm8LHg9ok6p3OVfVA==" + }, "dunder-proto@1.0.1": { "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dependencies": [ @@ -8108,6 +8133,9 @@ "github-slugger@2.0.0": { "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" }, + "gl-matrix@3.4.4": { + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==" + }, "glob-parent@5.1.2": { "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dependencies": [ @@ -9278,6 +9306,9 @@ "vary" ] }, + "ktx-parse@1.0.0": { + "integrity": "sha512-Z31kVizz4DF/6vo9YiSYVBhuXAfyQy9bGxlW3+mB5OELoZjfXVZQpRoctsx8IEDKxBd6SagXKo7qRvu38i8Jfg==" + }, "kuler@2.0.0": { "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, @@ -11488,6 +11519,9 @@ "read" ] }, + "property-graph@3.0.0": { + "integrity": "sha512-TnzxUsttmGtw+OiU0LDw+0FlMbJ8vV8pOjyDI7+Kdni4Tj0hW5BFh7TatQu7Y68hcvvFmiFOHilKShsA4R82fA==" + }, "proto-list@1.2.4": { "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, @@ -14977,6 +15011,7 @@ "dependencies": [ "jsr:@oak/oak@^17.1.3", "jsr:@std/http@^1.0.10", + "jsr:@std/path@^1.0.6", "npm:@deno/vite-plugin@1", "npm:vite@^5.4.8" ], diff --git a/src/client/core/Networking.ts b/src/client/core/Networking.ts index b47973f2..3a01aecb 100644 --- a/src/client/core/Networking.ts +++ b/src/client/core/Networking.ts @@ -438,7 +438,7 @@ export class Networking { } } - private lastThrownItemTimestamp: number; + private lastThrownItemTimestamp: number = 0; public broadcastThrownItem(trajectory: Trajectory) { this.socket.emit('throwItem', { diff --git a/src/client/core/RemoteItemRenderer.ts b/src/client/core/RemoteItemRenderer.ts index 4660a1f0..8e7da0c0 100644 --- a/src/client/core/RemoteItemRenderer.ts +++ b/src/client/core/RemoteItemRenderer.ts @@ -102,9 +102,10 @@ export class RemoteItemRenderer { this.itemsToRender = this.itemsToRender.filter((item) => { const existsInNewData = newWorldItemsData.some((worldItemData) => worldItemData.id === item.id); const createdOnClient = item.id > 1000000000; // Assuming server IDs are below this threshold - const createdRecently = item.item.getTrajectoryDuration() && + const trajectoryDuration = item.item.getTrajectoryDuration(); + const createdRecently = trajectoryDuration !== undefined && (Date.now() / 1000 - item.item.getCreationTimestamp() < - item.item.getTrajectoryDuration() + this.renderer.getLatency() / 1000 * 1.2 + 0.15); + trajectoryDuration + this.renderer.getLatency() / 1000 * 1.2 + 0.15); const shouldKeep = existsInNewData || (createdOnClient && createdRecently); if (!shouldKeep) item.item.destroy(); diff --git a/src/client/core/RemotePlayerRenderer.ts b/src/client/core/RemotePlayerRenderer.ts index 0444ac6e..2153aafd 100644 --- a/src/client/core/RemotePlayerRenderer.ts +++ b/src/client/core/RemotePlayerRenderer.ts @@ -480,7 +480,7 @@ export class RemotePlayerRenderer { private normalizeHealthIndicatorColor( color: PlayerData['healthIndicatorColor'], ): [number, number, number] { - const [r = 255, g = 255, b = 255] = color; + const [r, g, b] = color ?? [255, 255, 255]; return [r, g, b]; } diff --git a/src/client/input/CollisionManager.ts b/src/client/input/CollisionManager.ts index 270728ad..41bf1cc5 100644 --- a/src/client/input/CollisionManager.ts +++ b/src/client/input/CollisionManager.ts @@ -27,7 +27,7 @@ export class CollisionManager { private static dynamicColliders: THREE.Object3D[] = []; // Store collidable prop objects private inputHandler: InputHandler; - private particleSystem: ParticleSystem; // Optional particle system for effects + private particleSystem?: ParticleSystem; // Optional particle system for effects private networking: Networking; // Networking instance for accessing remote players private static readonly maxAngle: number = Math.cos(45 * Math.PI / 180); private readonly triNormal: THREE.Vector3; // Used for world-space calculations (map) and local for props before conversion @@ -195,7 +195,7 @@ export class CollisionManager { // Emit trajectory breadcrumbs for (const p of traj.points) { - this.particleSystem.emit({ + this.particleSystem?.emit({ position: p, count: 1, velocity: new THREE.Vector3(), @@ -228,7 +228,7 @@ export class CollisionManager { const p = center.clone() .addScaledVector(u, fx * halfSize) .addScaledVector(v, fy * halfSize); - this.particleSystem.emit({ + this.particleSystem?.emit({ position: p, count: 1, velocity: new THREE.Vector3(), diff --git a/src/client/items/ItemBase.ts b/src/client/items/ItemBase.ts index 4285947e..ff10f97d 100644 --- a/src/client/items/ItemBase.ts +++ b/src/client/items/ItemBase.ts @@ -112,7 +112,10 @@ export class ItemBase { this.addedToWorldScene = true; } - this.object.visible = !(this.initTrajectory && this.playerIdsTrajectoryHiddenFrom?.includes(this.localPlayerId)); //hide if trajectory is active and local player is in the hidden list + const hideForLocalPlayer = this.initTrajectory && + this.localPlayerId !== undefined && + this.playerIdsTrajectoryHiddenFrom?.includes(this.localPlayerId); + this.object.visible = !hideForLocalPlayer; // hide if trajectory active and local player should not see it if (this.initTrajectory) { if (this.trajectoryDuration === undefined) { this.trajectoryDuration = this.initTrajectory.getDuration(); diff --git a/src/server/DataValidator.ts b/src/server/DataValidator.ts index 48333704..53a5a1ea 100644 --- a/src/server/DataValidator.ts +++ b/src/server/DataValidator.ts @@ -119,6 +119,7 @@ export class DataValidator { url: z.string(), position: this.vector3Schema, velocity: this.vector3Schema, + angularVelocity: this.vector3Schema, quaternion: this.quaternionSchema, scale: this.vector3Schema, id: z.number(), diff --git a/src/server/GameEngine.ts b/src/server/GameEngine.ts index 37d1df4c..de9263fc 100644 --- a/src/server/GameEngine.ts +++ b/src/server/GameEngine.ts @@ -34,6 +34,7 @@ export class GameEngine { private tickProfileTime: number = 0; private cleanupProfileSamples: number = 0; private cleanupProfileTime: number = 0; + private lastServerTickTime: number = Date.now() / 1000; constructor( public playerManager: PlayerManager, @@ -54,10 +55,13 @@ export class GameEngine { private serverTick() { try { const currentTime = Date.now() / 1000; + const deltaTime = Math.max(0, currentTime - this.lastServerTickTime); + this.lastServerTickTime = currentTime; + this.playerManager.regenerateHealth(); this.playerManager.updateItemDurabilities(currentTime); this.itemManager.tick(currentTime); - this.propManager.onTick(currentTime); + this.propManager.onTick(deltaTime); if (this.gamemode) this.gamemode.tick(); const doFullEmit = currentTime - this.lastFullPlayerEmitTimestamp > (config.server.fullPlayerEmitInterval / 1000) || diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index ac56d39e..ff8542c4 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -16,6 +16,7 @@ import { PropManager } from './managers/PropManager.ts'; import { setupDevClientVersion } from './dev.ts'; import { WorldItem } from './models/WorldItem.ts'; import { Vector3 } from 'three'; +import { PhysicsEngine } from './physics/PhysicsEngine.ts'; export class GameServer { router: Router; @@ -48,7 +49,8 @@ export class GameServer { this.chatManager = new ChatManager(this.io, this.playerManager); this.itemManager = new ItemManager(this.mapData, this.playerManager, this.chatManager); this.damageSystem = new DamageSystem(this.playerManager, this.chatManager); - this.propManager = new PropManager(); + const physicsEngine = await PhysicsEngine.create(this.mapData.name); + this.propManager = new PropManager(physicsEngine); this.playerManager.setItemManager(this.itemManager); diff --git a/src/server/config.ts b/src/server/config.ts index 0c617d70..a7c139f7 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -104,55 +104,67 @@ async function updateEnvFile(defaults: Record) { } // Parse specific types from string values +const numberOr = (value: string | undefined, fallback: number): number => { + const parsed = Number.parseInt(value ?? '', 10); + return Number.isNaN(parsed) ? fallback : parsed; +}; + +const boolOr = (value: string | undefined, fallback: string): boolean => { + return (value ?? fallback).toLowerCase() === 'true'; +}; + function parseConfig(env: Record) { return { server: { - port: parseInt(env.PORT), - hostname: env.SERVER_HOSTNAME, + port: numberOr(env.PORT, parseInt(defaults.PORT, 10)), + hostname: env.SERVER_HOSTNAME || defaults.SERVER_HOSTNAME, name: env.SERVER_NAME, url: env.DOKPLOY_DEPLOY_URL ? 'https://' + env.DOKPLOY_DEPLOY_URL : env.SERVER_URL, defaultMap: env.SERVER_DEFAULT_MAP, - tickRate: parseInt(env.SERVER_TICK_RATE), - cleanupInterval: parseInt(env.SERVER_CLEANUP_INTERVAL), - fullPlayerEmitInterval: parseInt(env.FULL_PLAYER_EMIT_INTERVAL), + tickRate: numberOr(env.SERVER_TICK_RATE, parseInt(defaults.SERVER_TICK_RATE, 10)), + cleanupInterval: numberOr(env.SERVER_CLEANUP_INTERVAL, parseInt(defaults.SERVER_CLEANUP_INTERVAL, 10)), + fullPlayerEmitInterval: numberOr(env.FULL_PLAYER_EMIT_INTERVAL, parseInt(defaults.FULL_PLAYER_EMIT_INTERVAL, 10)), }, peer: { - updateInterval: parseInt(env.PEER_UPDATE_TICK_INTERVAL), - shareInterval: parseInt(env.PEER_SHARE_INTERVAL), - maxFailedAttempts: parseInt(env.PEER_MAX_FAILED_ATTEMPTS), - staleThreshold: parseInt(env.PEER_STALE_THRESHOLD), - maxServers: parseInt(env.PEER_MAX_SERVERS), - healthcheckRetries: parseInt(env.PEER_HEALTHCHECK_RETRIES), - healthcheckInterval: parseInt(env.PEER_HEALTHCHECK_INTERVAL), - urlFailureForgetTime: parseInt(env.PEER_URL_FAILURE_FORGET_TIME), + updateInterval: numberOr(env.PEER_UPDATE_TICK_INTERVAL, parseInt(defaults.PEER_UPDATE_TICK_INTERVAL, 10)), + shareInterval: numberOr(env.PEER_SHARE_INTERVAL, parseInt(defaults.PEER_SHARE_INTERVAL, 10)), + maxFailedAttempts: numberOr(env.PEER_MAX_FAILED_ATTEMPTS, parseInt(defaults.PEER_MAX_FAILED_ATTEMPTS, 10)), + staleThreshold: numberOr(env.PEER_STALE_THRESHOLD, parseInt(defaults.PEER_STALE_THRESHOLD, 10)), + maxServers: numberOr(env.PEER_MAX_SERVERS, parseInt(defaults.PEER_MAX_SERVERS, 10)), + healthcheckRetries: numberOr(env.PEER_HEALTHCHECK_RETRIES, parseInt(defaults.PEER_HEALTHCHECK_RETRIES, 10)), + healthcheckInterval: numberOr(env.PEER_HEALTHCHECK_INTERVAL, parseInt(defaults.PEER_HEALTHCHECK_INTERVAL, 10)), + urlFailureForgetTime: numberOr( + env.PEER_URL_FAILURE_FORGET_TIME, + parseInt(defaults.PEER_URL_FAILURE_FORGET_TIME, 10), + ), verifiedDomains: env.PEER_VERIFIED_DOMAINS ? env.PEER_VERIFIED_DOMAINS.split(',').map((domain) => domain.trim()) : [], }, game: { mode: env.GAME_MODE, - maxPlayers: parseInt(env.GAME_MAX_PLAYERS), - respawnDelay: parseInt(env.RESPAWN_DELAY), - pointsToWin: parseInt(env.POINTS_TO_WIN), - pointsToEvent: parseInt(env.POINTS_TO_EVENT), - minPlayersToStart: parseInt(env.MIN_PLAYERS_TO_START), + maxPlayers: numberOr(env.GAME_MAX_PLAYERS, parseInt(defaults.GAME_MAX_PLAYERS, 10)), + respawnDelay: numberOr(env.RESPAWN_DELAY, parseInt(defaults.RESPAWN_DELAY, 10)), + pointsToWin: numberOr(env.POINTS_TO_WIN, parseInt(defaults.POINTS_TO_WIN, 10)), + pointsToEvent: numberOr(env.POINTS_TO_EVENT, parseInt(defaults.POINTS_TO_EVENT, 10)), + minPlayersToStart: numberOr(env.MIN_PLAYERS_TO_START, parseInt(defaults.MIN_PLAYERS_TO_START, 10)), }, player: { - disconnectTime: parseInt(env.PLAYER_DISCONNECT_TIME), - afkKickTime: parseInt(env.PLAYER_AFK_KICK_TIME), - maxHealth: parseInt(env.PLAYER_MAX_HEALTH), - baseInventory: JSON.parse(env.PLAYER_BASE_INVENTORY) as InventoryItem[], + disconnectTime: numberOr(env.PLAYER_DISCONNECT_TIME, parseInt(defaults.PLAYER_DISCONNECT_TIME, 10)), + afkKickTime: numberOr(env.PLAYER_AFK_KICK_TIME, parseInt(defaults.PLAYER_AFK_KICK_TIME, 10)), + maxHealth: numberOr(env.PLAYER_MAX_HEALTH, parseInt(defaults.PLAYER_MAX_HEALTH, 10)), + baseInventory: JSON.parse(env.PLAYER_BASE_INVENTORY ?? defaults.PLAYER_BASE_INVENTORY) as InventoryItem[], }, health: { - regenDelay: parseInt(env.HEALTH_REGEN_DELAY), - regenRate: parseInt(env.HEALTH_REGEN_RATE), + regenDelay: numberOr(env.HEALTH_REGEN_DELAY, parseInt(defaults.HEALTH_REGEN_DELAY, 10)), + regenRate: numberOr(env.HEALTH_REGEN_RATE, parseInt(defaults.HEALTH_REGEN_RATE, 10)), }, items: { - maxItemsInWorld: parseInt(env.MAX_ITEMS_IN_WORLD), - respawnTime: parseInt(env.ITEM_RESPAWN_TIME), - despawnTime: parseInt(env.ITEM_DESPAWN_TIME), - shotsTakeDurability: (env.ITEM_SHOTS_DO_DURABILITY.toLowerCase() === 'true'), - rotTakesDurability: (env.ITEM_ROT_TAKES_DURABILITY.toLowerCase() === 'true'), + maxItemsInWorld: numberOr(env.MAX_ITEMS_IN_WORLD, parseInt(defaults.MAX_ITEMS_IN_WORLD, 10)), + respawnTime: numberOr(env.ITEM_RESPAWN_TIME, parseInt(defaults.ITEM_RESPAWN_TIME, 10)), + despawnTime: numberOr(env.ITEM_DESPAWN_TIME, parseInt(defaults.ITEM_DESPAWN_TIME, 10)), + shotsTakeDurability: boolOr(env.ITEM_SHOTS_DO_DURABILITY, defaults.ITEM_SHOTS_DO_DURABILITY), + rotTakesDurability: boolOr(env.ITEM_ROT_TAKES_DURABILITY, defaults.ITEM_ROT_TAKES_DURABILITY), }, dev: { appendClientHashToVersion: env.APPEND_CLIENT_HASH_TO_VERSION === 'true', diff --git a/src/server/gamemodes/KingOfTheHillGamemode.ts b/src/server/gamemodes/KingOfTheHillGamemode.ts index 5fa0f4de..7f8a42f7 100644 --- a/src/server/gamemodes/KingOfTheHillGamemode.ts +++ b/src/server/gamemodes/KingOfTheHillGamemode.ts @@ -420,7 +420,7 @@ export class KingOfTheHillGamemode extends FFAGamemode { /** * Resets all players after a win: respawns them and clears their inventories. */ - resetAfterWin(): void { + public override resetAfterWin(): void { for (const player of this.gameEngine.playerManager.getAllPlayers()) { // Respawn the player this.gameEngine.playerManager.respawnPlayer(player); @@ -449,7 +449,7 @@ export class KingOfTheHillGamemode extends FFAGamemode { /** * Resets the game by clearing points, removing the flag, and respawning it. */ - resetGame(): void { + public override resetGame(): void { console.log('🔄 Resetting Solo CTF game...'); for (const player of this.gameEngine.playerManager.getAllPlayers()) { const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id); diff --git a/src/server/gamemodes/SoloCTFGamemode.ts b/src/server/gamemodes/SoloCTFGamemode.ts index 751f6dbe..22c630cd 100644 --- a/src/server/gamemodes/SoloCTFGamemode.ts +++ b/src/server/gamemodes/SoloCTFGamemode.ts @@ -346,7 +346,7 @@ export class SoloCTFGamemode extends FFAGamemode { /** * Resets the game by clearing points, removing the flag, and respawning it. */ - resetGame(): void { + public override resetGame(): void { console.log('🔄 Resetting Solo CTF game...'); for (const player of this.gameEngine.playerManager.getAllPlayers()) { const extras = this.gameEngine.playerManager.getPlayerExtrasById(player.id); diff --git a/src/server/managers/ChatManager.ts b/src/server/managers/ChatManager.ts index e6cc7f7f..97f48e66 100644 --- a/src/server/managers/ChatManager.ts +++ b/src/server/managers/ChatManager.ts @@ -34,9 +34,11 @@ export class ChatManager { break; case 'kill': { const player = this.playerManager.getPlayerById(playerId); - if (player) { - this.playerManager.respawnPlayer(player); + if (!player) { + this.whisperEventMessage('&cUnable to find you to respawn.', socket); + break; } + this.playerManager.respawnPlayer(player); this.broadcastEventMessage(`&c${player.name} ^b ${player.name}`); break; } diff --git a/src/server/managers/ItemManager.ts b/src/server/managers/ItemManager.ts index 53411225..76165b8f 100644 --- a/src/server/managers/ItemManager.ts +++ b/src/server/managers/ItemManager.ts @@ -101,9 +101,12 @@ export class ItemManager { // If the item already exists, just increase the reserve count const existingItem = player.inventory.find((inv) => inv.itemId === id); if (existingItem) { - existingItem.durabilityOffset += item.durabilityOffset + 1 || 1; - if (existingItem.durabilityOffset + existingItem.durability > 1) { - existingItem.durabilityOffset -= 1; + const incomingOffset = (item.durabilityOffset ?? 0) + 1; + const currentOffset = existingItem.durabilityOffset ?? 0; + const updatedOffset = currentOffset + incomingOffset; + existingItem.durabilityOffset = updatedOffset; + if (updatedOffset + existingItem.durability > 1) { + existingItem.durabilityOffset = updatedOffset - 1; existingItem.overflow += 1; } shouldPickup = true; diff --git a/src/server/managers/PropManager.ts b/src/server/managers/PropManager.ts index 9a27e8d9..b1db643d 100644 --- a/src/server/managers/PropManager.ts +++ b/src/server/managers/PropManager.ts @@ -1,6 +1,7 @@ import { Prop, PropData } from '../../shared/Prop.ts'; import * as THREE from 'three'; import { PropDamageRequest } from '../models/PropDamageRequest.ts'; +import { PhysicsEngine } from '../physics/PhysicsEngine.ts'; export class PropManager { private props: Map = new Map(); @@ -8,25 +9,29 @@ export class PropManager { // public testProp!: Prop; - constructor() { + constructor(private physics: PhysicsEngine) { // this.testProp = this.addProp('models/hexagon.glb', new THREE.Vector3(0, 0.5, 0)); // this.addProp('models/hexagon.glb', new THREE.Vector3(0, 10, 0)); } - public onTick(_deltaTime: number) { - // this.testProp.position.x = Math.sin(Date.now() / 1000) * 3; - // this.testProp.velocity.x = Math.cos(Date.now() / 1000) * 3; + public onTick(deltaTime: number) { + this.physics.step(deltaTime); } - public addProp( + public async addProp( url: string, position: THREE.Vector3, quaternion: THREE.Quaternion = new THREE.Quaternion(), scale: THREE.Vector3 = new THREE.Vector3(1, 1, 1), - ): Prop { + ): Promise { const prop = new Prop(url, position, quaternion, scale); this.props.set(prop.id, prop); this.hasUpdates = true; + try { + await this.physics.registerProp(prop); + } catch (error) { + console.error(`⚠️ [Server] Failed to register physics for prop ${prop.id}:`, error); + } console.log(`📦 [Server] Prop added: ID ${prop.id}, URL ${url}`); return prop; } @@ -47,6 +52,7 @@ export class PropManager { public removeProp(id: number): boolean { const success = this.props.delete(id); if (success) { + this.physics.removeProp(id); this.hasUpdates = true; console.log(`🗑️ [Server] Prop removed: ID ${id}`); } @@ -61,6 +67,10 @@ export class PropManager { return Array.from(this.props.values()).map((prop) => prop.toJSON()); } + public getPropCount(): number { + return this.props.size; + } + public clearUpdatesFlag(): void { this.hasUpdates = false; } diff --git a/src/server/physics/PhysicsEngine.ts b/src/server/physics/PhysicsEngine.ts new file mode 100644 index 00000000..c72cb2a0 --- /dev/null +++ b/src/server/physics/PhysicsEngine.ts @@ -0,0 +1,324 @@ +import RAPIER from 'npm:@dimforge/rapier3d-compat'; +import { NodeIO } from 'npm:@gltf-transform/core'; +import { + EXTTextureWebP, + KHRDracoMeshCompression, + KHRMaterialsSpecular, + KHRTextureTransform, +} from 'npm:@gltf-transform/extensions'; +import type { Mesh, Node } from 'npm:@gltf-transform/core'; +import { mat4, quat, vec3 } from 'npm:gl-matrix'; +import { fromFileUrl } from '@std/path/from-file-url'; +import type { Prop } from '../../shared/Prop.ts'; + +interface MeshData { + vertices: Float32Array; + indices?: Uint32Array; +} + +interface PropPhysicsEntry { + prop: Prop; + bodyHandle: RAPIER.RigidBodyHandle; + colliderHandles: RAPIER.ColliderHandle[]; + meshKey: string; +} + +const DEFAULT_GRAVITY = { x: 0, y: -9.81, z: 0 }; +const FIXED_TIMESTEP = 1 / 60; + +/** + * Headless Rapier physics integration that loads static map geometry and dynamic prop meshes. + * Converts GLB assets into collider meshes on the fly and keeps Prop instances in sync after each simulation step. + */ +export class PhysicsEngine { + private readonly world: RAPIER.World; + private readonly io: NodeIO; + private readonly meshCache = new Map(); + private readonly props = new Map(); + private accumulator = 0; + + private constructor(world: RAPIER.World, io: NodeIO) { + this.world = world; + this.io = io; + } + + static async create(mapName: string): Promise { + await RAPIER.init(); + const world = new RAPIER.World(DEFAULT_GRAVITY); + const { default: draco3d } = await import('npm:draco3dgltf@1.5.6'); + const decoderModule = await draco3d.createDecoderModule(); + const io = new NodeIO() + .registerExtensions([ + KHRTextureTransform, + EXTTextureWebP, + KHRMaterialsSpecular, + KHRDracoMeshCompression, + ]) + .registerDependencies({ + 'draco3d.decoder': decoderModule, + }); + const engine = new PhysicsEngine(world, io); + await engine.loadStaticMap(mapName); + return engine; + } + + async registerProp(prop: Prop): Promise { + if (!prop.doPhysics) return; + + const meshKey = this.normalizeMeshKey(prop.url); + const meshData = await this.getMeshData(meshKey); + const scaledVertices = this.scaleVertices(meshData.vertices, prop.scale.x, prop.scale.y, prop.scale.z); + + const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(prop.position.x, prop.position.y, prop.position.z) + .setRotation({ + x: prop.quaternion.x, + y: prop.quaternion.y, + z: prop.quaternion.z, + w: prop.quaternion.w, + }) + .setLinvel(prop.velocity.x, prop.velocity.y, prop.velocity.z) + .setAngvel({ x: prop.angularVelocity.x, y: prop.angularVelocity.y, z: prop.angularVelocity.z }); + + const rigidBody = this.world.createRigidBody(rigidBodyDesc); + + const colliderDesc = this.buildDynamicCollider(scaledVertices); + colliderDesc.setFriction(0.8); + colliderDesc.setRestitution(0.1); + const collider = this.world.createCollider(colliderDesc, rigidBody); + + this.props.set(prop.id, { + prop, + bodyHandle: rigidBody.handle, + colliderHandles: [collider.handle], + meshKey, + }); + + // Ensure the prop reflects the authoritative body state immediately. + this.syncPropFromBody(prop, rigidBody); + } + + removeProp(propId: number): void { + const entry = this.props.get(propId); + if (!entry) return; + const body = this.world.getRigidBody(entry.bodyHandle); + if (body) { + this.world.removeRigidBody(body); + } + this.props.delete(propId); + } + + step(deltaSeconds: number): void { + if (deltaSeconds <= 0) { + this.syncAllProps(); + return; + } + + this.accumulator += deltaSeconds; + while (this.accumulator >= FIXED_TIMESTEP) { + this.world.step(); + this.accumulator -= FIXED_TIMESTEP; + } + + this.syncAllProps(); + } + + private async loadStaticMap(mapName: string): Promise { + const meshKey = this.normalizeMeshKey(`maps/${mapName}/map.glb`); + const meshData = await this.getMeshData(meshKey); + const indices = meshData.indices ?? this.buildSequentialIndices(meshData.vertices.length / 3); + const colliderDesc = RAPIER.ColliderDesc.trimesh(meshData.vertices, indices); + colliderDesc.setFriction(0.9); + colliderDesc.setRestitution(0.0); + this.world.createCollider(colliderDesc); // Static collider (no rigid body handle passed) + } + + private syncAllProps(): void { + for (const entry of this.props.values()) { + const body = this.world.getRigidBody(entry.bodyHandle); + if (!body) continue; + this.syncPropFromBody(entry.prop, body); + } + } + + private syncPropFromBody(prop: Prop, body: RAPIER.RigidBody): void { + const translation = body.translation(); + prop.position.set(translation.x, translation.y, translation.z); + + const rotation = body.rotation(); + prop.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w); + + const linvel = body.linvel(); + prop.velocity.set(linvel.x, linvel.y, linvel.z); + + const angvel = body.angvel(); + prop.angularVelocity.set(angvel.x, angvel.y, angvel.z); + } + + private scaleVertices(source: Float32Array, sx: number, sy: number, sz: number): Float32Array { + const result = new Float32Array(source.length); + for (let i = 0; i < source.length; i += 3) { + result[i] = source[i] * sx; + result[i + 1] = source[i + 1] * sy; + result[i + 2] = source[i + 2] * sz; + } + return result; + } + + private buildSequentialIndices(vertexCount: number): Uint32Array { + const indices = new Uint32Array(vertexCount); + for (let i = 0; i < vertexCount; i++) { + indices[i] = i; + } + return indices; + } + + private buildDynamicCollider(vertices: Float32Array): RAPIER.ColliderDesc { + if (vertices.length >= 12) { // Minimum for a convex hull (4 points) + const convex = RAPIER.ColliderDesc.convexHull(vertices); + if (convex) return convex; + } + + // Fallback to a spherical approximation if convex hull fails (e.g., degenerate geometry) + let radius = 0.5; + if (vertices.length >= 3) { + let maxSq = 0; + for (let i = 0; i < vertices.length; i += 3) { + const x = vertices[i]; + const y = vertices[i + 1]; + const z = vertices[i + 2]; + const lenSq = x * x + y * y + z * z; + if (lenSq > maxSq) maxSq = lenSq; + } + radius = Math.sqrt(maxSq); + } + return RAPIER.ColliderDesc.ball(Math.max(radius, 0.1)); + } + + private async getMeshData(meshKey: string): Promise { + const cached = this.meshCache.get(meshKey); + if (cached) return cached; + + const absolutePath = this.resolveDistPath(meshKey); + const document = await this.io.read(absolutePath); + const root = document.getRoot(); + const scenes = root.listScenes(); + + const vertices: number[] = []; + const indices: number[] = []; + let hasIndices = false; + + for (const scene of scenes) { + for (const child of scene.listChildren()) { + this.collectMeshData(child, mat4.create(), vertices, indices, () => { + hasIndices = true; + }); + } + } + + const meshData: MeshData = { + vertices: new Float32Array(vertices), + indices: hasIndices ? new Uint32Array(indices) : undefined, + }; + this.meshCache.set(meshKey, meshData); + return meshData; + } + + private collectMeshData( + node: Node, + parentMatrix: mat4, + vertexAccumulator: number[], + indexAccumulator: number[], + setHasIndices: () => void, + ): void { + const worldMatrix = mat4.create(); + mat4.multiply(worldMatrix, parentMatrix, this.getNodeMatrix(node)); + + const mesh = node.getMesh(); + if (mesh) { + this.extractMeshPrimitives(mesh, worldMatrix, vertexAccumulator, indexAccumulator, setHasIndices); + } + + for (const child of node.listChildren()) { + this.collectMeshData(child, worldMatrix, vertexAccumulator, indexAccumulator, setHasIndices); + } + } + + private extractMeshPrimitives( + mesh: Mesh, + worldMatrix: mat4, + vertexAccumulator: number[], + indexAccumulator: number[], + setHasIndices: () => void, + ): void { + for (const primitive of mesh.listPrimitives()) { + const positionAccessor = primitive.getAttribute('POSITION'); + if (!positionAccessor) continue; + + const positions = positionAccessor.getArray(); + if (!positions) continue; + + const transformedVertices: number[] = []; + const temp = vec3.create(); + + for (let i = 0; i < positions.length; i += 3) { + vec3.set(temp, positions[i], positions[i + 1], positions[i + 2]); + vec3.transformMat4(temp, temp, worldMatrix); + transformedVertices.push(temp[0], temp[1], temp[2]); + } + + const vertexOffset = vertexAccumulator.length / 3; + vertexAccumulator.push(...transformedVertices); + + const indexAccessor = primitive.getIndices(); + if (indexAccessor) { + const rawIndices = indexAccessor.getArray(); + if (!rawIndices) continue; + setHasIndices(); + for (let i = 0; i < rawIndices.length; i++) { + indexAccumulator.push(vertexOffset + rawIndices[i]); + } + } else { + // Assume triangle lists if no indices are provided. + const mode = primitive.getMode(); + if (mode !== null && mode !== undefined && mode !== 4) { + continue; + } + for (let i = 0; i < transformedVertices.length / 3; i++) { + indexAccumulator.push(vertexOffset + i); + } + setHasIndices(); + } + } + } + + private getNodeMatrix(node: Node): mat4 { + const matrix = node.getMatrix(); + if (matrix) { + const out = mat4.create(); + const values = matrix as unknown as number[]; + for (let i = 0; i < 16; i++) { + out[i] = values[i]; + } + return out; + } + + const translation = node.getTranslation() ?? [0, 0, 0]; + const rotation = node.getRotation() ?? [0, 0, 0, 1]; + const scale = node.getScale() ?? [1, 1, 1]; + + const out = mat4.create(); + return mat4.fromRotationTranslationScale(out, rotation as quat, translation as vec3, scale as vec3); + } + + private resolveDistPath(relative: string): string { + const normalized = relative.replace(/^\/+/, ''); + const baseUrl = new URL('../../../dist/', import.meta.url); + const fileUrl = new URL(normalized, baseUrl); + return fromFileUrl(fileUrl); + } + + private normalizeMeshKey(url: string): string { + return url.replace(/^\/+/, ''); + } +} diff --git a/src/shared/Prop.ts b/src/shared/Prop.ts index 39525991..9d22ccd3 100644 --- a/src/shared/Prop.ts +++ b/src/shared/Prop.ts @@ -10,6 +10,7 @@ export class Prop { public velocity = new THREE.Vector3(0, 0, 0); public quaternion = new THREE.Quaternion(); public scale = new THREE.Vector3(1, 1, 1); + public angularVelocity = new THREE.Vector3(0, 0, 0); public id = Math.floor(Math.random() * 1000000000); //unique id public name = ''; public doPhysics: boolean = true; @@ -38,6 +39,7 @@ export class Prop { ...this, position: serializableVec3(this.position), velocity: serializableVec3(this.velocity), + angularVelocity: serializableVec3(this.angularVelocity), quaternion: serializableQuaternion(this.quaternion), scale: serializableVec3(this.scale), }; diff --git a/src/shared/Utils.ts b/src/shared/Utils.ts index d512aa41..5fef1751 100644 --- a/src/shared/Utils.ts +++ b/src/shared/Utils.ts @@ -2,7 +2,7 @@ export function lerp(start: number, end: number, t: number): number { return start + (end - start) * t; } -export function seededRandom(seed) { +export function seededRandom(seed: number): number { const x = Math.sin(seed++) * 100000; return x - Math.floor(x); } From 6c21eaaac1840711861ceb86f49595ebf8387191 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:39:27 -0400 Subject: [PATCH 03/20] docs: add idea run commands --- .run/dev.run.xml | 17 +++++++++++++++++ .run/start.run.xml | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .run/dev.run.xml create mode 100644 .run/start.run.xml diff --git a/.run/dev.run.xml b/.run/dev.run.xml new file mode 100644 index 00000000..ae2483ce --- /dev/null +++ b/.run/dev.run.xml @@ -0,0 +1,17 @@ + + + + diff --git a/.run/start.run.xml b/.run/start.run.xml new file mode 100644 index 00000000..b16270af --- /dev/null +++ b/.run/start.run.xml @@ -0,0 +1,17 @@ + + + + From 5412fb7d13d296546cab3431fc858ee819e06bd2 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:59:57 -0400 Subject: [PATCH 04/20] chore: upgrade to deno 2.5.4 --- .github/workflows/ci.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18be3dfe..87ae35de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: id-token: write # To enable use of OIDC for npm provenance env: - DENO_VERSION: '2.4.2' + DENO_VERSION: '2.5.4' jobs: # Build Job diff --git a/Dockerfile b/Dockerfile index 17601157..52b7dc42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Define Deno version (using the version from your current Dockerfile) -ARG DENO_VERSION=2.4.2 +ARG DENO_VERSION=2.5.4 ARG BIN_IMAGE=denoland/deno:bin-${DENO_VERSION} # Stage to get the Deno binary From 96b104e2cda308703b97ff6ec84ee0031d0fcb3d Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:16:44 -0400 Subject: [PATCH 05/20] chore: update imports to work w/ new Deno version --- deno.json | 13 ++++++++++++- deno.lock | 9 +++++++++ import_map.json | 14 +++++++++++++- src/server/DataValidator.ts | 2 +- src/server/GameServer.ts | 4 ++-- src/server/dev.ts | 4 ++-- src/server/physics/PhysicsEngine.ts | 12 ++++++------ src/shared/Player.ts | 2 +- src/shared/Prop.ts | 2 +- src/shared/messages.ts | 2 +- 10 files changed, 48 insertions(+), 16 deletions(-) diff --git a/deno.json b/deno.json index e380f267..80511f5c 100644 --- a/deno.json +++ b/deno.json @@ -18,11 +18,22 @@ "lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"] }, "imports": { + "@dimforge/rapier3d-compat": "npm:@dimforge/rapier3d-compat@0.16.2", + "@gltf-transform/core": "npm:@gltf-transform/core@4.1.3", + "@gltf-transform/extensions": "npm:@gltf-transform/extensions@4.1.3", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0", "@oak/oak": "jsr:@oak/oak@^17.1.3", "@std/http": "jsr:@std/http@^1.0.10", "@std/path": "jsr:@std/path@^1.0.6", - "vite": "npm:vite@^5.4.8" + "@std/path/from-file-url": "jsr:@std/path@^1.0.6/from-file-url", + "draco3dgltf": "npm:draco3dgltf@1.5.6", + "gl-matrix": "npm:gl-matrix@3.4.4", + "socket_io": "https://deno.land/x/socket_io@0.2.0/mod.ts", + "std/crypto": "https://deno.land/std@0.207.0/crypto/mod.ts", + "std/http/server": "https://deno.land/std@0.150.0/http/server.ts", + "std/fs/walk": "https://deno.land/std@0.207.0/fs/walk.ts", + "vite": "npm:vite@^5.4.8", + "zod": "https://deno.land/x/zod@v3.23.8/mod.ts" }, "lint": { "include": ["src/"], diff --git a/deno.lock b/deno.lock index b046b18e..c896b0ed 100644 --- a/deno.lock +++ b/deno.lock @@ -32,8 +32,11 @@ "npm:@angular/router@19": "19.2.14_@angular+common@19.2.14__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_@angular+core@19.2.14__rxjs@7.8.2__zone.js@0.15.1_@angular+platform-browser@19.2.14__@angular+animations@19.2.14___@angular+common@19.2.14____@angular+core@19.2.14_____rxjs@7.8.2_____zone.js@0.15.1____rxjs@7.8.2____zone.js@0.15.1___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1_rxjs@7.8.2_zone.js@0.15.1_@angular+animations@19.2.14__@angular+common@19.2.14___@angular+core@19.2.14____rxjs@7.8.2____zone.js@0.15.1___rxjs@7.8.2___zone.js@0.15.1__@angular+core@19.2.14___rxjs@7.8.2___zone.js@0.15.1__rxjs@7.8.2__zone.js@0.15.1", "npm:@deno/vite-plugin@1": "1.0.5_vite@5.4.19__less@4.2.2__sass@1.85.0__terser@5.39.0_@types+node@22.12.0", "npm:@dimforge/rapier3d-compat@*": "0.16.2", + "npm:@dimforge/rapier3d-compat@0.16.2": "0.16.2", "npm:@gltf-transform/core@*": "4.1.3", + "npm:@gltf-transform/core@4.1.3": "4.1.3", "npm:@gltf-transform/extensions@*": "4.1.3", + "npm:@gltf-transform/extensions@4.1.3": "4.1.3", "npm:@semantic-release/git@^10.0.1": "10.0.1_semantic-release@24.2.7__typescript@5.5.4__marked@15.0.12_typescript@5.5.4", "npm:@types/node@*": "22.12.0", "npm:@types/three@0.169": "0.169.0", @@ -43,6 +46,7 @@ "npm:fdir@^6.4.2": "6.4.6_picomatch@4.0.2", "npm:front-matter@^4.0.2": "4.0.2", "npm:gl-matrix@*": "3.4.4", + "npm:gl-matrix@3.4.4": "3.4.4", "npm:joi@^17.13.3": "17.13.3", "npm:jsdom@22": "22.1.0", "npm:marked-gfm-heading-id@^3.1.0": "3.2.0_marked@5.1.2", @@ -15013,6 +15017,11 @@ "jsr:@std/http@^1.0.10", "jsr:@std/path@^1.0.6", "npm:@deno/vite-plugin@1", + "npm:@dimforge/rapier3d-compat@0.16.2", + "npm:@gltf-transform/core@4.1.3", + "npm:@gltf-transform/extensions@4.1.3", + "npm:draco3dgltf@1.5.6", + "npm:gl-matrix@3.4.4", "npm:vite@^5.4.8" ], "packageJson": { diff --git a/import_map.json b/import_map.json index 6037fe65..08ca7448 100644 --- a/import_map.json +++ b/import_map.json @@ -1,8 +1,20 @@ { "imports": { + "@dimforge/rapier3d-compat": "npm:@dimforge/rapier3d-compat@0.16.2", + "@gltf-transform/core": "npm:@gltf-transform/core@4.1.3", + "@gltf-transform/extensions": "npm:@gltf-transform/extensions@4.1.3", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0", "@oak/oak": "jsr:@oak/oak@^17.1.3", "@std/http": "jsr:@std/http@^1.0.10", - "vite": "npm:vite@^5.4.8" + "@std/path": "jsr:@std/path@^1.0.6", + "@std/path/from-file-url": "jsr:@std/path@^1.0.6/from-file-url", + "draco3dgltf": "npm:draco3dgltf@1.5.6", + "gl-matrix": "npm:gl-matrix@3.4.4", + "socket_io": "https://deno.land/x/socket_io@0.2.0/mod.ts", + "std/crypto": "https://deno.land/std@0.207.0/crypto/mod.ts", + "std/http/server": "https://deno.land/std@0.150.0/http/server.ts", + "std/fs/walk": "https://deno.land/std@0.207.0/fs/walk.ts", + "vite": "npm:vite@^5.4.8", + "zod": "https://deno.land/x/zod@v3.23.8/mod.ts" } } diff --git a/src/server/DataValidator.ts b/src/server/DataValidator.ts index 53a5a1ea..411aa277 100644 --- a/src/server/DataValidator.ts +++ b/src/server/DataValidator.ts @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts'; +import { z } from 'zod'; import { ChatMessage } from './models/ChatMessage.ts'; import { DamageRequest } from './models/DamageRequest.ts'; import { Player, PlayerData } from '../shared/Player.ts'; diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index ff8542c4..49bb2f50 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -1,7 +1,7 @@ import { Application, Router, send } from '@oak/oak'; -import { Server } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; +import { Server } from 'socket_io'; +import { serve } from 'std/http/server'; import config from './config.ts'; -import { serve } from 'https://deno.land/std@0.150.0/http/server.ts'; import { GameEngine } from './GameEngine.ts'; import { PlayerManager } from './managers/PlayerManager.ts'; diff --git a/src/server/dev.ts b/src/server/dev.ts index e6259b16..38fd874f 100644 --- a/src/server/dev.ts +++ b/src/server/dev.ts @@ -1,5 +1,5 @@ -import { crypto } from 'https://deno.land/std@0.207.0/crypto/mod.ts'; -import { walk } from 'https://deno.land/std@0.207.0/fs/walk.ts'; +import { crypto } from 'std/crypto'; +import { walk } from 'std/fs/walk'; async function getDirectoryHash(path: string): Promise { const filePaths: string[] = []; diff --git a/src/server/physics/PhysicsEngine.ts b/src/server/physics/PhysicsEngine.ts index c72cb2a0..987f3938 100644 --- a/src/server/physics/PhysicsEngine.ts +++ b/src/server/physics/PhysicsEngine.ts @@ -1,13 +1,13 @@ -import RAPIER from 'npm:@dimforge/rapier3d-compat'; -import { NodeIO } from 'npm:@gltf-transform/core'; +import RAPIER from '@dimforge/rapier3d-compat'; +import { NodeIO } from '@gltf-transform/core'; import { EXTTextureWebP, KHRDracoMeshCompression, KHRMaterialsSpecular, KHRTextureTransform, -} from 'npm:@gltf-transform/extensions'; -import type { Mesh, Node } from 'npm:@gltf-transform/core'; -import { mat4, quat, vec3 } from 'npm:gl-matrix'; +} from '@gltf-transform/extensions'; +import type { Mesh, Node } from '@gltf-transform/core'; +import { mat4, quat, vec3 } from 'gl-matrix'; import { fromFileUrl } from '@std/path/from-file-url'; import type { Prop } from '../../shared/Prop.ts'; @@ -45,7 +45,7 @@ export class PhysicsEngine { static async create(mapName: string): Promise { await RAPIER.init(); const world = new RAPIER.World(DEFAULT_GRAVITY); - const { default: draco3d } = await import('npm:draco3dgltf@1.5.6'); + const { default: draco3d } = await import('draco3dgltf'); const decoderModule = await draco3d.createDecoderModule(); const io = new NodeIO() .registerExtensions([ diff --git a/src/shared/Player.ts b/src/shared/Player.ts index 8a109b01..124a7d98 100644 --- a/src/shared/Player.ts +++ b/src/shared/Player.ts @@ -1,4 +1,4 @@ -import type { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts'; +import type { z } from 'zod'; import { DataValidator } from '../server/DataValidator.ts'; import * as THREE from 'three'; import type { InventoryItem } from './InventoryItem.ts'; diff --git a/src/shared/Prop.ts b/src/shared/Prop.ts index 9d22ccd3..38676c7c 100644 --- a/src/shared/Prop.ts +++ b/src/shared/Prop.ts @@ -1,4 +1,4 @@ -import type { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts'; +import type { z } from 'zod'; import { DataValidator } from '../server/DataValidator.ts'; import * as THREE from 'three'; diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 24f6e5dc..7566cde6 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -1,4 +1,4 @@ -import type { Server, Socket } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; +import type { Server, Socket } from 'socket_io'; import type { Socket as ClientSocket } from 'socket.io-client'; import type { ChatMessage } from '../server/models/ChatMessage.ts'; import type { ServerInfo } from '../server/models/ServerInfo.ts'; From aafd68e52461d18d7a62b77f34b9c3a3bd35f28d Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Sat, 25 Oct 2025 02:02:59 -0400 Subject: [PATCH 06/20] feat: init world with props --- public/maps/alpha_1/map.json | 3 +- public/maps/crackhouse_1/map.json | 12 +++- public/maps/deathmatch_1/map.json | 3 +- public/maps/empty/map.json | 10 +++ src/server/GameServer.ts | 1 + src/server/managers/PropManager.ts | 41 ++++++++++++ src/server/models/MapData.ts | 101 ++++++++++++++++++++++++----- 7 files changed, 150 insertions(+), 21 deletions(-) diff --git a/public/maps/alpha_1/map.json b/public/maps/alpha_1/map.json index a339cea6..010210bd 100644 --- a/public/maps/alpha_1/map.json +++ b/public/maps/alpha_1/map.json @@ -57,5 +57,6 @@ "position": { "x": -1, "y": 0.25, "z": -6 }, "itemId": 3 } - ] + ], + "props": [] } diff --git a/public/maps/crackhouse_1/map.json b/public/maps/crackhouse_1/map.json index 14e9ce1c..734c2507 100644 --- a/public/maps/crackhouse_1/map.json +++ b/public/maps/crackhouse_1/map.json @@ -227,5 +227,15 @@ "position": { "x": -4.71, "y": 3.24, "z": 10.22 }, "itemId": 1 } - ] + ], + "props": [ + { + "url": "/models/simplified_possum.glb", + "position": { "x": 9, "y": 5, "z": -5 }, + "scale": { "x": 1, "y": 1, "z": 1 }, + "name": "big guy", + "doPhysics": true, + "playersCollide": true + } + ] } diff --git a/public/maps/deathmatch_1/map.json b/public/maps/deathmatch_1/map.json index eee186d2..3168d8a7 100644 --- a/public/maps/deathmatch_1/map.json +++ b/public/maps/deathmatch_1/map.json @@ -83,5 +83,6 @@ "position": { "x": -1, "y": 0.25, "z": -6 }, "itemId": 2 } - ] + ], + "props": [] } diff --git a/public/maps/empty/map.json b/public/maps/empty/map.json index 97d9081e..3066a44d 100644 --- a/public/maps/empty/map.json +++ b/public/maps/empty/map.json @@ -11,5 +11,15 @@ "position": { "x": 0, "y": 5, "z": 0 }, "itemId": 1 } + ], + "props": [ + { + "url": "/models/hexagon.glb", + "position": { "x": 0, "y": 1.5, "z": 0 }, + "scale": { "x": 5, "y": 5, "z": 5 }, + "name": "Showcase Hexagon", + "doPhysics": false, + "playersCollide": false + } ] } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 49bb2f50..fc7cc90d 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -51,6 +51,7 @@ export class GameServer { this.damageSystem = new DamageSystem(this.playerManager, this.chatManager); const physicsEngine = await PhysicsEngine.create(this.mapData.name); this.propManager = new PropManager(physicsEngine); + await this.propManager.loadInitialProps(this.mapData.props); this.playerManager.setItemManager(this.itemManager); diff --git a/src/server/managers/PropManager.ts b/src/server/managers/PropManager.ts index b1db643d..6d0c8ffd 100644 --- a/src/server/managers/PropManager.ts +++ b/src/server/managers/PropManager.ts @@ -2,6 +2,16 @@ import { Prop, PropData } from '../../shared/Prop.ts'; import * as THREE from 'three'; import { PropDamageRequest } from '../models/PropDamageRequest.ts'; import { PhysicsEngine } from '../physics/PhysicsEngine.ts'; +import type { PropSpawn } from '../models/MapData.ts'; + +type PropInitOptions = { + name?: string; + doPhysics?: boolean; + playersCollide?: boolean; + health?: number; + velocity?: THREE.Vector3; + angularVelocity?: THREE.Vector3; +}; export class PropManager { private props: Map = new Map(); @@ -23,8 +33,16 @@ export class PropManager { position: THREE.Vector3, quaternion: THREE.Quaternion = new THREE.Quaternion(), scale: THREE.Vector3 = new THREE.Vector3(1, 1, 1), + options: PropInitOptions = {}, ): Promise { const prop = new Prop(url, position, quaternion, scale); + if (options.name !== undefined) prop.name = options.name; + if (options.doPhysics !== undefined) prop.doPhysics = options.doPhysics; + if (options.playersCollide !== undefined) prop.playersCollide = options.playersCollide; + if (options.health !== undefined) prop.health = options.health; + if (options.velocity) prop.velocity.copy(options.velocity); + if (options.angularVelocity) prop.angularVelocity.copy(options.angularVelocity); + this.props.set(prop.id, prop); this.hasUpdates = true; try { @@ -74,4 +92,27 @@ export class PropManager { public clearUpdatesFlag(): void { this.hasUpdates = false; } + + public async loadInitialProps(spawns: PropSpawn[]): Promise { + if (!spawns.length) return; + await Promise.all( + spawns.map(async (spawn) => { + const options: PropInitOptions = { + name: spawn.name, + doPhysics: spawn.doPhysics, + playersCollide: spawn.playersCollide, + health: spawn.health, + velocity: spawn.velocity?.clone(), + angularVelocity: spawn.angularVelocity?.clone(), + }; + await this.addProp( + spawn.url, + spawn.position.clone(), + spawn.quaternion.clone(), + spawn.scale.clone(), + options, + ); + }), + ); + } } diff --git a/src/server/models/MapData.ts b/src/server/models/MapData.ts index 8e359075..f8671d20 100644 --- a/src/server/models/MapData.ts +++ b/src/server/models/MapData.ts @@ -2,12 +2,32 @@ import { RespawnPoint } from './RespawnPoint.ts'; import { ItemRespawnPoint } from './ItemRespawnPoint.ts'; import * as THREE from 'three'; +interface Vector3JSON { + x: number; + y: number; + z: number; +} + +interface QuaternionJSON { + x: number; + y: number; + z: number; + w: number; +} + +interface RespawnPointJSON { + position: Vector3JSON; + quaternion: QuaternionJSON; +} + +interface ItemRespawnPointJSON { + position: Vector3JSON; + itemId: number; + spawnChancePerTick?: number; +} + interface CapturePointJSON { - position: { - x: number; - y: number; - z: number; - }; + position: Vector3JSON; scale: number; } @@ -16,23 +36,51 @@ interface CapturePoint { scale: number; } +interface PropSpawnJSON { + url: string; + position: Vector3JSON; + quaternion?: QuaternionJSON; + scale?: Vector3JSON; + name?: string; + doPhysics?: boolean; + playersCollide?: boolean; + health?: number; + velocity?: Vector3JSON; + angularVelocity?: Vector3JSON; +} + +export interface PropSpawn { + url: string; + position: THREE.Vector3; + quaternion: THREE.Quaternion; + scale: THREE.Vector3; + name?: string; + doPhysics: boolean; + playersCollide: boolean; + health?: number; + velocity?: THREE.Vector3; + angularVelocity?: THREE.Vector3; +} + +interface MapJSON { + name: string; + respawnPoints?: RespawnPointJSON[]; + itemRespawnPoints?: ItemRespawnPointJSON[]; + capturePoints?: CapturePointJSON[]; + props?: PropSpawnJSON[]; +} + export class MapData { constructor( public name: string, public respawnPoints: RespawnPoint[], public itemRespawnPoints: ItemRespawnPoint[], public capturePoints: CapturePoint[] = [], + public props: PropSpawn[] = [], ) {} - static fromJSON( - json: { - capturePoints: CapturePointJSON[]; - respawnPoints: RespawnPoint[]; - itemRespawnPoints: ItemRespawnPoint[]; - name: string; - }, - ): MapData { - const respawnPoints = json.respawnPoints.map( + static fromJSON(json: MapJSON): MapData { + const respawnPoints = (json.respawnPoints ?? []).map( (rp) => new RespawnPoint( new THREE.Vector3(rp.position.x, rp.position.y, rp.position.z), @@ -40,22 +88,39 @@ export class MapData { ), ); - const itemRespawnPoints = json.itemRespawnPoints.map( + const itemRespawnPoints = (json.itemRespawnPoints ?? []).map( (irp) => new ItemRespawnPoint( new THREE.Vector3(irp.position.x, irp.position.y, irp.position.z), irp.itemId, - irp.spawnChancePerTick, + irp.spawnChancePerTick ?? 1, ), ); - const capturePoints = json.capturePoints.map( + const capturePoints = (json.capturePoints ?? []).map( (cp) => ({ position: new THREE.Vector3(cp.position.x, cp.position.y, cp.position.z), scale: cp.scale, }), ); - return new MapData(json.name, respawnPoints, itemRespawnPoints, capturePoints); + const props = (json.props ?? []).map((prop) => ({ + url: prop.url, + position: new THREE.Vector3(prop.position.x, prop.position.y, prop.position.z), + quaternion: prop.quaternion + ? new THREE.Quaternion(prop.quaternion.x, prop.quaternion.y, prop.quaternion.z, prop.quaternion.w) + : new THREE.Quaternion(), + scale: prop.scale ? new THREE.Vector3(prop.scale.x, prop.scale.y, prop.scale.z) : new THREE.Vector3(1, 1, 1), + name: prop.name, + doPhysics: prop.doPhysics ?? true, + playersCollide: prop.playersCollide ?? true, + health: prop.health, + velocity: prop.velocity ? new THREE.Vector3(prop.velocity.x, prop.velocity.y, prop.velocity.z) : undefined, + angularVelocity: prop.angularVelocity + ? new THREE.Vector3(prop.angularVelocity.x, prop.angularVelocity.y, prop.angularVelocity.z) + : undefined, + })); + + return new MapData(json.name, respawnPoints, itemRespawnPoints, capturePoints, props); } } From 3413f4563baefaf048f7030eea7d5a0780c6e141 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Sat, 25 Oct 2025 02:22:10 -0400 Subject: [PATCH 07/20] feat: apply impulse when prop is shot --- public/maps/crackhouse_1/map.json | 14 ++++++++++ src/server/GameServer.ts | 2 +- src/server/managers/PropManager.ts | 42 ++++++++++++++++++++++++++--- src/server/physics/PhysicsEngine.ts | 14 ++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/public/maps/crackhouse_1/map.json b/public/maps/crackhouse_1/map.json index 734c2507..ed381e44 100644 --- a/public/maps/crackhouse_1/map.json +++ b/public/maps/crackhouse_1/map.json @@ -236,6 +236,20 @@ "name": "big guy", "doPhysics": true, "playersCollide": true + }, { + "url": "/models/simplified_possum.glb", + "position": { "x": 9, "y": 4, "z": -5 }, + "scale": { "x": 1, "y": 1, "z": 1 }, + "name": "big guy", + "doPhysics": true, + "playersCollide": true + }, { + "url": "/models/simplified_possum.glb", + "position": { "x": 9, "y": 6, "z": -5 }, + "scale": { "x": 1, "y": 1, "z": 1 }, + "name": "big guy", + "doPhysics": true, + "playersCollide": true } ] } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index fc7cc90d..c671ee56 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -50,7 +50,7 @@ export class GameServer { this.itemManager = new ItemManager(this.mapData, this.playerManager, this.chatManager); this.damageSystem = new DamageSystem(this.playerManager, this.chatManager); const physicsEngine = await PhysicsEngine.create(this.mapData.name); - this.propManager = new PropManager(physicsEngine); + this.propManager = new PropManager(physicsEngine, this.playerManager); await this.propManager.loadInitialProps(this.mapData.props); this.playerManager.setItemManager(this.itemManager); diff --git a/src/server/managers/PropManager.ts b/src/server/managers/PropManager.ts index 6d0c8ffd..1836f891 100644 --- a/src/server/managers/PropManager.ts +++ b/src/server/managers/PropManager.ts @@ -3,6 +3,7 @@ import * as THREE from 'three'; import { PropDamageRequest } from '../models/PropDamageRequest.ts'; import { PhysicsEngine } from '../physics/PhysicsEngine.ts'; import type { PropSpawn } from '../models/MapData.ts'; +import { PlayerManager } from './PlayerManager.ts'; type PropInitOptions = { name?: string; @@ -13,13 +14,17 @@ type PropInitOptions = { angularVelocity?: THREE.Vector3; }; +const PROP_SHOT_BASE_IMPULSE = 0; +const PROP_SHOT_DAMAGE_MULTIPLIER = 0.1; +const PROP_SHOT_VERTICAL_BOOST = 0; + export class PropManager { private props: Map = new Map(); public hasUpdates: boolean = false; //triggers an early full data broadcast.. mostly for when adding and deleting props // public testProp!: Prop; - constructor(private physics: PhysicsEngine) { + constructor(private physics: PhysicsEngine, private playerManager: PlayerManager) { // this.testProp = this.addProp('models/hexagon.glb', new THREE.Vector3(0, 0.5, 0)); // this.addProp('models/hexagon.glb', new THREE.Vector3(0, 10, 0)); } @@ -56,15 +61,20 @@ export class PropManager { public handleDamageRequest(data: PropDamageRequest) { const prop = this.props.get(data.targetPropID); - if (prop && prop.health !== undefined) { + if (!prop) { + console.log(`⚠️ [Server] Prop not found for damage request: ID ${data.targetPropID}`); + return; + } + + if (prop.health !== undefined) { prop.health -= data.damage; console.log(`💥 [Server] Prop damaged: ID ${data.targetPropID}, Health ${prop.health}`); if (prop.health <= 0) { this.removeProp(data.targetPropID); } - } else { - console.log(`⚠️ [Server] Prop not found for damage request: ID ${data.targetPropID}`); } + + this.applyShotImpulse(prop, data.playerID, data.damage); } public removeProp(id: number): boolean { @@ -115,4 +125,28 @@ export class PropManager { }), ); } + + private applyShotImpulse(prop: Prop, playerId: number, damage: number): void { + if (!prop.doPhysics) return; + + const impulseDir = this.getShotDirection(playerId); + if (impulseDir.lengthSq() === 0) return; + + const magnitude = PROP_SHOT_BASE_IMPULSE + Math.max(0, damage) * PROP_SHOT_DAMAGE_MULTIPLIER; + const impulse = impulseDir.multiplyScalar(magnitude); + impulse.y += PROP_SHOT_VERTICAL_BOOST; + + this.physics.applyImpulse(prop.id, impulse); + this.hasUpdates = true; + } + + private getShotDirection(playerId: number): THREE.Vector3 { + const player = this.playerManager.getPlayerById(playerId); + if (!player) return new THREE.Vector3(0, 0, -1); + const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.lookQuaternion); + if (forward.lengthSq() === 0) { + return new THREE.Vector3(0, 0, -1); + } + return forward.normalize(); + } } diff --git a/src/server/physics/PhysicsEngine.ts b/src/server/physics/PhysicsEngine.ts index 987f3938..1906cd73 100644 --- a/src/server/physics/PhysicsEngine.ts +++ b/src/server/physics/PhysicsEngine.ts @@ -11,6 +11,12 @@ import { mat4, quat, vec3 } from 'gl-matrix'; import { fromFileUrl } from '@std/path/from-file-url'; import type { Prop } from '../../shared/Prop.ts'; +interface Vector3Like { + x: number; + y: number; + z: number; +} + interface MeshData { vertices: Float32Array; indices?: Uint32Array; @@ -98,6 +104,14 @@ export class PhysicsEngine { this.syncPropFromBody(prop, rigidBody); } + applyImpulse(propId: number, impulse: Vector3Like): void { + const entry = this.props.get(propId); + if (!entry) return; + const body = this.world.getRigidBody(entry.bodyHandle); + if (!body) return; + body.applyImpulse({ x: impulse.x, y: impulse.y, z: impulse.z }, true); + } + removeProp(propId: number): void { const entry = this.props.get(propId); if (!entry) return; From a1bb611a477db1698bd1129d3090182b751b6466 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Sat, 25 Oct 2025 02:27:41 -0400 Subject: [PATCH 08/20] feat: players repel props --- src/server/managers/PropManager.ts | 39 +++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/server/managers/PropManager.ts b/src/server/managers/PropManager.ts index 1836f891..5d0c8e55 100644 --- a/src/server/managers/PropManager.ts +++ b/src/server/managers/PropManager.ts @@ -16,7 +16,11 @@ type PropInitOptions = { const PROP_SHOT_BASE_IMPULSE = 0; const PROP_SHOT_DAMAGE_MULTIPLIER = 0.1; -const PROP_SHOT_VERTICAL_BOOST = 0; +const PROP_SHOT_VERTICAL_BOOST = 0.03; + +const PROP_PLAYER_REPEL_RADIUS = 0.75; +const PROP_PLAYER_REPEL_STRENGTH = 0.2; +const PROP_PLAYER_REPEL_VERTICAL_BIAS = 0; export class PropManager { private props: Map = new Map(); @@ -30,6 +34,7 @@ export class PropManager { } public onTick(deltaTime: number) { + this.applyPlayerRepulsion(); this.physics.step(deltaTime); } @@ -149,4 +154,36 @@ export class PropManager { } return forward.normalize(); } + + private applyPlayerRepulsion(): void { + const players = this.playerManager.getAllPlayers(); + if (!players.length) return; + + const impulseAccumulator = new THREE.Vector3(); + const directionBuffer = new THREE.Vector3(); + + for (const prop of this.props.values()) { + if (!prop.doPhysics) continue; + + let hasImpulse = false; + impulseAccumulator.set(0, 0, 0); + + for (const player of players) { + directionBuffer.copy(prop.position).sub(player.position); + const distance = directionBuffer.length(); + if (distance === 0 || distance > PROP_PLAYER_REPEL_RADIUS) continue; + + const falloff = 1 - distance / PROP_PLAYER_REPEL_RADIUS; + directionBuffer.normalize(); + impulseAccumulator.addScaledVector(directionBuffer, PROP_PLAYER_REPEL_STRENGTH * falloff); + impulseAccumulator.y += PROP_PLAYER_REPEL_VERTICAL_BIAS * falloff; + hasImpulse = true; + } + + if (hasImpulse && impulseAccumulator.lengthSq() > 0) { + this.physics.applyImpulse(prop.id, impulseAccumulator); + this.hasUpdates = true; + } + } + } } From 5bc43006da02d363646d52d72d12db52e0780e45 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Sat, 25 Oct 2025 02:32:58 -0400 Subject: [PATCH 09/20] chose: smaller shot impulse constants --- public/maps/crackhouse_1/map.json | 3 +++ src/server/managers/PropManager.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/maps/crackhouse_1/map.json b/public/maps/crackhouse_1/map.json index ed381e44..ff2a0c43 100644 --- a/public/maps/crackhouse_1/map.json +++ b/public/maps/crackhouse_1/map.json @@ -234,6 +234,7 @@ "position": { "x": 9, "y": 5, "z": -5 }, "scale": { "x": 1, "y": 1, "z": 1 }, "name": "big guy", + "health": 100000, "doPhysics": true, "playersCollide": true }, { @@ -241,6 +242,7 @@ "position": { "x": 9, "y": 4, "z": -5 }, "scale": { "x": 1, "y": 1, "z": 1 }, "name": "big guy", + "health": 100000, "doPhysics": true, "playersCollide": true }, { @@ -248,6 +250,7 @@ "position": { "x": 9, "y": 6, "z": -5 }, "scale": { "x": 1, "y": 1, "z": 1 }, "name": "big guy", + "health": 100000, "doPhysics": true, "playersCollide": true } diff --git a/src/server/managers/PropManager.ts b/src/server/managers/PropManager.ts index 5d0c8e55..e55b9285 100644 --- a/src/server/managers/PropManager.ts +++ b/src/server/managers/PropManager.ts @@ -15,8 +15,8 @@ type PropInitOptions = { }; const PROP_SHOT_BASE_IMPULSE = 0; -const PROP_SHOT_DAMAGE_MULTIPLIER = 0.1; -const PROP_SHOT_VERTICAL_BOOST = 0.03; +const PROP_SHOT_DAMAGE_MULTIPLIER = 0.05; +const PROP_SHOT_VERTICAL_BOOST = 0.01; const PROP_PLAYER_REPEL_RADIUS = 0.75; const PROP_PLAYER_REPEL_STRENGTH = 0.2; From 2ed187853a32ea6c53a0890fdeb13ac2905def2b Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:50:08 -0500 Subject: [PATCH 10/20] revert: set durability defaults to off --- src/client/core/Networking.ts | 2 ++ src/client/ui/ChatOverlay.ts | 5 +++-- src/server/config.ts | 2 +- src/server/models/ServerInfo.ts | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/client/core/Networking.ts b/src/client/core/Networking.ts index 3a01aecb..cacb1e0d 100644 --- a/src/client/core/Networking.ts +++ b/src/client/core/Networking.ts @@ -41,6 +41,7 @@ export interface ServerInfo { memUsageHeapTotal: number; memUsageExternal: number; idleKickTime: number; + durabilityEnabled: boolean; } interface LastUploadedLocalPlayer { @@ -116,6 +117,7 @@ export class Networking { memUsageHeapTotal: 0, memUsageExternal: 0, idleKickTime: 60, + durabilityEnabled: false, }; this.setupSocketListeners(); diff --git a/src/client/ui/ChatOverlay.ts b/src/client/ui/ChatOverlay.ts index 7f15910d..7fcc3646 100644 --- a/src/client/ui/ChatOverlay.ts +++ b/src/client/ui/ChatOverlay.ts @@ -1499,10 +1499,11 @@ export class ChatOverlay { ctx.save(); ctx.globalAlpha = 0.5; - // If the item doesn't actually use durability (e.g., flag), smoothly hide the bar. + // If the item doesn't actually use durability (e.g., flag) or durability is disabled server-side, hide the bar. // Otherwise, always lerp toward the aggregated target (overflow + fractional durability). const usesDurability = (item.lifetime !== undefined) || (item.shotsAvailable !== undefined); - if (!usesDurability) { + const durabilityEnabled = this.networking.getServerInfo().durabilityEnabled; + if (!usesDurability || !durabilityEnabled) { this.durabilityLerpable = lerp(this.durabilityLerpable, 0, 0.5 * this.deltaTime * 60); if (this.durabilityLerpable < 0.05) { ctx.restore(); diff --git a/src/server/config.ts b/src/server/config.ts index a7c139f7..7ead1d67 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -45,7 +45,7 @@ const defaults = { ITEM_RESPAWN_TIME: '5', ITEM_DESPAWN_TIME: '300', //5 minutes, set to 0 to disable ITEM_SHOTS_DO_DURABILITY: 'false', - ITEM_ROT_TAKES_DURABILITY: 'true', + ITEM_ROT_TAKES_DURABILITY: 'false', //platform-specific settings DOKPLOY_DEPLOY_URL: '', diff --git a/src/server/models/ServerInfo.ts b/src/server/models/ServerInfo.ts index 9ec29ee2..2503ea7d 100644 --- a/src/server/models/ServerInfo.ts +++ b/src/server/models/ServerInfo.ts @@ -18,6 +18,7 @@ export class ServerInfo { public memUsageHeapTotal: number = 0; public memUsageExternal: number = 0; public idleKickTime: number; + public durabilityEnabled: boolean; constructor() { this.name = config.server.name; @@ -28,6 +29,7 @@ export class ServerInfo { this.playerMaxHealth = config.player.maxHealth; this.url = config.server.url; this.idleKickTime = config.player.afkKickTime; + this.durabilityEnabled = config.items.shotsTakeDurability || config.items.rotTakesDurability; } toJSON() { return { From d6608c8edf94ca4ed205ac51857f353cb53ecba5 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:59:18 -0500 Subject: [PATCH 11/20] ci: properly cache deno deps --- Dockerfile | 84 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52b7dc42..7c8d2614 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,59 +1,73 @@ -# Define Deno version (using the version from your current Dockerfile) +# syntax=docker/dockerfile:1.7-labs ARG DENO_VERSION=2.5.4 ARG BIN_IMAGE=denoland/deno:bin-${DENO_VERSION} -# Stage to get the Deno binary +# Stage: get the Deno binary FROM ${BIN_IMAGE} AS bin -# Stage to get tini init system -FROM buildpack-deps:20.04-curl AS tini +# Stage: get tini +FROM buildpack-deps:22.04-curl AS tini ARG TINI_VERSION=0.19.0 ARG TARGETARCH -# Download tini for the target architecture RUN curl -fsSL https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-${TARGETARCH} \ --output /tini \ && chmod +x /tini -# Final runtime stage using distroless/cc for a minimal image -FROM gcr.io/distroless/cc - -# Set the working directory inside the container +# Stage: builder +FROM debian:bookworm-slim AS builder WORKDIR /app +ENV DENO_DIR=/deno-dir +RUN mkdir -p "$DENO_DIR" -# Set Deno environment variables -ENV DENO_DIR=/deno-dir/ -ENV DENO_INSTALL_ROOT=/usr/local -ARG DENO_VERSION -ENV DENO_VERSION=${DENO_VERSION} - -# Copy the Deno binary from the 'bin' stage +# Copy Deno binary into builder COPY --from=bin /deno /bin/deno -# Copy tini from the 'tini' stage -COPY --from=tini /tini /tini +# Copy dependency manifests first for better layer caching +COPY deno.json import_map.json ./ -# Copy Deno configuration files -COPY deno.json . -COPY import_map.json . +# Cache dependencies before copying source (cache survives source changes) +RUN --mount=type=cache,target=/tmp/deno-cache bash -c "set -euo pipefail \ + && export DENO_DIR=/tmp/deno-cache \ + && /bin/deno cache --import-map=import_map.json main.ts || true" -# Copy the rest of your application source code +# Copy rest of app COPY . . -RUN ["/bin/deno", "task", "build"] +# Build and cache final deps +RUN --mount=type=cache,target=/tmp/deno-cache bash -c "set -euo pipefail \ + && export DENO_DIR=/tmp/deno-cache \ + && /bin/deno task build \ + && /bin/deno task cache \ + && mkdir -p /deno-dir \ + && rm -rf /deno-dir/* \ + && cp -a /tmp/deno-cache/. /deno-dir" + +# Final runtime stage +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* -# Cache application dependencies using Deno -# This step downloads and caches the modules needed by main.ts -# Use exec form because distroless doesn't have /bin/sh -#RUN ["/bin/deno", "cache", "main.ts"] -RUN ["/bin/deno", "task", "cache"] +RUN useradd --uid 1993 --user-group deno \ + && mkdir -p /deno-dir /app -# Expose the port your application listens on +ENV DENO_DIR=/deno-dir +ENV DENO_INSTALL_ROOT=/usr/local +ARG DENO_VERSION +ENV DENO_VERSION=${DENO_VERSION} + +COPY --from=bin /deno /usr/bin/deno +COPY --from=tini /tini /tini +RUN chmod +x /usr/bin/deno /tini + +COPY --chown=deno:deno --from=builder /app /app +COPY --chown=deno:deno --from=builder /deno-dir /deno-dir + +WORKDIR /app EXPOSE 3000 -# Set the entrypoint to run Deno via tini -# tini handles signal forwarding and zombie reaping -ENTRYPOINT ["/tini", "--", "/bin/deno"] +USER deno -# Set the default command to run your main application file -# Includes the necessary permissions based on your 'start' task in deno.json -CMD ["run", "--allow-read", "--allow-env", "--allow-net", "--allow-write", "main.ts"] +ENTRYPOINT ["/tini", "--", "/usr/bin/deno"] +CMD ["task", "startnobuild"] From d1b00d659741b3bf56e0189ced42f7a396722e09 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:01:12 -0500 Subject: [PATCH 12/20] fmt: map.json --- public/maps/crackhouse_1/map.json | 56 ++++++++++++++++--------------- public/maps/empty/map.json | 4 +-- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/public/maps/crackhouse_1/map.json b/public/maps/crackhouse_1/map.json index ff2a0c43..487c8895 100644 --- a/public/maps/crackhouse_1/map.json +++ b/public/maps/crackhouse_1/map.json @@ -228,31 +228,33 @@ "itemId": 1 } ], - "props": [ - { - "url": "/models/simplified_possum.glb", - "position": { "x": 9, "y": 5, "z": -5 }, - "scale": { "x": 1, "y": 1, "z": 1 }, - "name": "big guy", - "health": 100000, - "doPhysics": true, - "playersCollide": true - }, { - "url": "/models/simplified_possum.glb", - "position": { "x": 9, "y": 4, "z": -5 }, - "scale": { "x": 1, "y": 1, "z": 1 }, - "name": "big guy", - "health": 100000, - "doPhysics": true, - "playersCollide": true - }, { - "url": "/models/simplified_possum.glb", - "position": { "x": 9, "y": 6, "z": -5 }, - "scale": { "x": 1, "y": 1, "z": 1 }, - "name": "big guy", - "health": 100000, - "doPhysics": true, - "playersCollide": true - } - ] + "props": [ + { + "url": "/models/simplified_possum.glb", + "position": { "x": 9, "y": 5, "z": -5 }, + "scale": { "x": 1, "y": 1, "z": 1 }, + "name": "big guy", + "health": 100000, + "doPhysics": true, + "playersCollide": true + }, + { + "url": "/models/simplified_possum.glb", + "position": { "x": 9, "y": 4, "z": -5 }, + "scale": { "x": 1, "y": 1, "z": 1 }, + "name": "big guy", + "health": 100000, + "doPhysics": true, + "playersCollide": true + }, + { + "url": "/models/simplified_possum.glb", + "position": { "x": 9, "y": 6, "z": -5 }, + "scale": { "x": 1, "y": 1, "z": 1 }, + "name": "big guy", + "health": 100000, + "doPhysics": true, + "playersCollide": true + } + ] } diff --git a/public/maps/empty/map.json b/public/maps/empty/map.json index 3066a44d..433e3149 100644 --- a/public/maps/empty/map.json +++ b/public/maps/empty/map.json @@ -15,8 +15,8 @@ "props": [ { "url": "/models/hexagon.glb", - "position": { "x": 0, "y": 1.5, "z": 0 }, - "scale": { "x": 5, "y": 5, "z": 5 }, + "position": { "x": 0, "y": 1.5, "z": 0 }, + "scale": { "x": 5, "y": 5, "z": 5 }, "name": "Showcase Hexagon", "doPhysics": false, "playersCollide": false From ee3c2a2410bee47377c8616854dd35178a425d42 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:05:19 -0500 Subject: [PATCH 13/20] fix: try/catch around .env write for prod --- src/server/config.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/server/config.ts b/src/server/config.ts index 7ead1d67..c5ef066d 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -94,12 +94,17 @@ async function updateEnvFile(defaults: Record) { } } - // Write the final effective configuration back to .env (optional but can be helpful for debugging) - const envContent = Object.entries(filteredFinalEnv) - .map(([key, value]) => `${key}=${value}`) - .join('\n'); - - await Deno.writeTextFile(envPath, envContent); + // Write the final effective configuration back to .env (optional, for local debugging only) + // Skip in production/containers where filesystem may be read-only + try { + const envContent = Object.entries(filteredFinalEnv) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + await Deno.writeTextFile(envPath, envContent); + } catch { + console.log('Failed to write .env file'); + // Silently ignore write failures (expected in containerized environments) + } return filteredFinalEnv; // Return the final, merged config } From c2354f96722918ae616adfe09857e1ba620e821e Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:10:45 -0500 Subject: [PATCH 14/20] fix: clear physics accum before map loads --- src/client/input/CollisionManager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client/input/CollisionManager.ts b/src/client/input/CollisionManager.ts index 41bf1cc5..f5bf49ae 100644 --- a/src/client/input/CollisionManager.ts +++ b/src/client/input/CollisionManager.ts @@ -25,6 +25,7 @@ export class CollisionManager { public static mapLoaded: boolean = false; private static staticMapBvh?: MeshBVH; // Store the BVH for the static map private static dynamicColliders: THREE.Object3D[] = []; // Store collidable prop objects + private physicsStarted: boolean = false; // Track if physics has started after map load private inputHandler: InputHandler; private particleSystem?: ParticleSystem; // Optional particle system for effects @@ -73,6 +74,12 @@ export class CollisionManager { return; } + // Reset clock on first physics frame after map loads to avoid huge deltaTime + if (!this.physicsStarted) { + this.clock.getDelta(); // Discard accumulated time + this.physicsStarted = true; + } + let deltaTime: number = this.clock.getDelta(); let steps: number = 1; // Iterative solving for more stable physics From b0884120bcb258939dffadf716907406c7a92b9e Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:14:44 -0500 Subject: [PATCH 15/20] fix: use built-in secrets.GITHUB_TOKEN --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87ae35de..20262947 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 with: - persist-credentials: 'false' fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v3 @@ -164,7 +163,7 @@ jobs: run: npm clean-install - name: Release env: - GITHUB_TOKEN: ${{ secrets.ACTIONS_DEPLOY_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GIT_AUTHOR_NAME: 'github-actions[bot]' GIT_AUTHOR_EMAIL: 'github-actions[bot]@users.noreply.github.com' GIT_COMMITTER_NAME: 'github-actions[bot]' From d86387f4589c2f1f90943decc3f39083771903be Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:09:51 -0500 Subject: [PATCH 16/20] chore: add idea docker run script --- .run/docker.run.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .run/docker.run.xml diff --git a/.run/docker.run.xml b/.run/docker.run.xml new file mode 100644 index 00000000..f27cf46a --- /dev/null +++ b/.run/docker.run.xml @@ -0,0 +1,17 @@ + + + + From 58af6d16099f09316511ddb39bf1b1b3cc2af232 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:39:01 -0500 Subject: [PATCH 17/20] feat: command auto-complete --- src/client/core/CommandManager.ts | 25 ++++++ src/client/ui/ChatOverlay.ts | 142 ++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/client/core/CommandManager.ts b/src/client/core/CommandManager.ts index 9e0bd880..5703a44f 100644 --- a/src/client/core/CommandManager.ts +++ b/src/client/core/CommandManager.ts @@ -175,6 +175,31 @@ export class CommandManager { } return match; } + + public getCommandNames(): string[] { + return this.commands.map((cmd) => cmd.getCmdName()); + } + + public getSuggestions(input: string): string[] { + if (!input.startsWith('/')) return []; + + const query = input.slice(1).toLowerCase().trim(); + const parts = query.split(' '); + + if (parts.length > 1) return []; + + const clientCommands = this.commands.map((cmd) => cmd.getCmdName()); + const serverCommands = ['help', 'kill', 'thumbsup', 'thumbsdown', 'octopus', 'goblin', 'ping', 'version', 'clear']; + const allCommands = [...new Set([...clientCommands, ...serverCommands])].sort(); + + if (query === '') { + return allCommands.map((cmd) => '/' + cmd); + } + + return allCommands + .filter((cmd) => cmd.startsWith(query)) + .map((cmd) => '/' + cmd); + } } class Command { diff --git a/src/client/ui/ChatOverlay.ts b/src/client/ui/ChatOverlay.ts index 7fcc3646..8ea9a70b 100644 --- a/src/client/ui/ChatOverlay.ts +++ b/src/client/ui/ChatOverlay.ts @@ -99,6 +99,11 @@ export class ChatOverlay { private containerElement: HTMLElement; private spriteManager: SpriteManager; + private tabSuggestions: string[] = []; + private tabSelectedIndex: number = 0; + private tabScrollOffset: number = 0; + private tabOriginalInput: string = ''; + private gameIndex: number; private offscreenCanvas: HTMLCanvasElement; @@ -545,6 +550,8 @@ export class ChatOverlay { if (this.localPlayer.chatActive) { if (this.localPlayer.chatMsg.startsWith('>')) { linesToRender.push('&2' + usermsg + cursor); + } else if (this.isValidCommand(this.localPlayer.chatMsg)) { + linesToRender.push('&a' + usermsg + cursor); } else { linesToRender.push(usermsg + cursor); } @@ -589,9 +596,55 @@ export class ChatOverlay { } ctx.fillRect(2, 200 - 20 - 7, width + 1, 9); } + + this.renderCommandSuggestions(ctx); ctx.globalAlpha = 1; } + private renderCommandSuggestions(ctx: CanvasRenderingContext2D) { + if (!this.localPlayer.chatActive || !this.localPlayer.chatMsg.startsWith('/')) return; + + if (this.isValidCommand(this.localPlayer.chatMsg)) return; + + const suggestions = this.tabSuggestions.length > 0 + ? this.tabSuggestions + : this.commandManager.getSuggestions(this.localPlayer.chatMsg); + if (suggestions.length === 0) return; + + const maxVisible = Math.min(suggestions.length, 8); + const lineHeight = 9; + const padding = 2; + const baseY = 200 - 20 - 8 - lineHeight * maxVisible - padding; + + ctx.font = '8px Tiny5'; + let maxWidth = 0; + for (let i = 0; i < maxVisible; i++) { + const idx = i + this.tabScrollOffset; + if (idx >= suggestions.length) break; + const w = ctx.measureText(suggestions[idx]).width; + if (w > maxWidth) maxWidth = w; + } + + ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'; + ctx.fillRect(2, baseY - padding, maxWidth + padding * 2 + 2, lineHeight * maxVisible + padding * 2); + + for (let i = 0; i < maxVisible; i++) { + const idx = i + this.tabScrollOffset; + if (idx >= suggestions.length) break; + const suggestion = suggestions[idx]; + const y = baseY + lineHeight * i + 7; + + const isSelected = this.tabSelectedIndex === idx; + + if (isSelected) { + ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; + ctx.fillRect(2, baseY + lineHeight * i, maxWidth + padding * 2 + 2, lineHeight); + } + + this.renderPixelText(suggestion, 4, y, '#aaaaaa'); + } + } + private renderPixelText( text: string, x: number, @@ -1858,6 +1911,22 @@ export class ChatOverlay { if (e.key === 'Backspace' && (this.localPlayer.chatActive || this.nameSettingActive)) { this.localPlayer.chatMsg = this.localPlayer.chatMsg.slice(0, -1); + this.resetTabCompletion(); + return; + } + + if (e.key === 'Tab' && this.localPlayer.chatActive && this.localPlayer.chatMsg.startsWith('/')) { + e.preventDefault(); + this.applyTabCompletion(); + return; + } + + if ( + (e.key === 'ArrowUp' || e.key === 'ArrowDown') && this.localPlayer.chatActive && + this.localPlayer.chatMsg.startsWith('/') + ) { + e.preventDefault(); + this.cycleTabSuggestion(e.key === 'ArrowUp'); return; } @@ -1875,16 +1944,19 @@ export class ChatOverlay { this.localPlayer.chatMsg = ''; this.localPlayer.chatActive = false; this.nameSettingActive = false; + this.resetTabCompletion(); } if (e.key === 'Escape' || e.key === 'Enter') { this.localPlayer.chatMsg = ''; this.localPlayer.chatActive = false; this.nameSettingActive = false; + this.resetTabCompletion(); } if ((this.localPlayer.chatActive) && e.key.length === 1 && this.localPlayer.chatMsg.length < 300) { this.localPlayer.chatMsg += e.key; + this.resetTabCompletion(); } if ((this.nameSettingActive) && e.key.length === 1 && this.localPlayer.chatMsg.length < 42) { @@ -1909,6 +1981,76 @@ export class ChatOverlay { } } + private updateTabSuggestions() { + const input = this.localPlayer.chatMsg; + this.tabSuggestions = this.commandManager.getSuggestions(input); + this.tabSelectedIndex = 0; + this.tabOriginalInput = input; + } + + private cycleTabSuggestion(up: boolean) { + if (this.tabSuggestions.length === 0) { + this.updateTabSuggestions(); + } + if (this.tabSuggestions.length === 0) return; + + const maxVisible = 8; + + if (up) { + this.tabSelectedIndex--; + if (this.tabSelectedIndex < 0) { + this.tabSelectedIndex = this.tabSuggestions.length - 1; + this.tabScrollOffset = Math.max(0, this.tabSuggestions.length - maxVisible); + } else if (this.tabSelectedIndex < this.tabScrollOffset) { + this.tabScrollOffset = this.tabSelectedIndex; + } + } else { + this.tabSelectedIndex++; + if (this.tabSelectedIndex >= this.tabSuggestions.length) { + this.tabSelectedIndex = 0; + this.tabScrollOffset = 0; + } else if (this.tabSelectedIndex >= this.tabScrollOffset + maxVisible) { + this.tabScrollOffset = this.tabSelectedIndex - maxVisible + 1; + } + } + } + + private applyTabCompletion() { + if (this.tabSuggestions.length === 0) { + this.updateTabSuggestions(); + } + if (this.tabSuggestions.length === 0) return; + + this.localPlayer.chatMsg = this.tabSuggestions[this.tabSelectedIndex]; + this.resetTabCompletion(); + } + + private resetTabCompletion() { + this.tabSuggestions = []; + this.tabSelectedIndex = 0; + this.tabScrollOffset = 0; + this.tabOriginalInput = ''; + } + + private isValidCommand(input: string): boolean { + if (!input.startsWith('/')) return false; + const cmd = input.slice(1).split(' ')[0].toLowerCase(); + if (cmd === '') return false; + const allCommands = [ + ...this.commandManager.getCommandNames(), + 'help', + 'kill', + 'thumbsup', + 'thumbsdown', + 'octopus', + 'goblin', + 'ping', + 'version', + 'clear', + ]; + return allCommands.includes(cmd); + } + public addChatMessage(msg: { id: number; name: string; message: string }) { const chatMessage: ChatMessage = { id: msg.id, From ae1352a4f24e13c9c2b2f37ef58a665ca7557663 Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:54:03 -0500 Subject: [PATCH 18/20] slightly better alpha --- src/client/ui/ChatOverlay.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/ui/ChatOverlay.ts b/src/client/ui/ChatOverlay.ts index 8ea9a70b..402421e2 100644 --- a/src/client/ui/ChatOverlay.ts +++ b/src/client/ui/ChatOverlay.ts @@ -597,8 +597,8 @@ export class ChatOverlay { ctx.fillRect(2, 200 - 20 - 7, width + 1, 9); } - this.renderCommandSuggestions(ctx); ctx.globalAlpha = 1; + this.renderCommandSuggestions(ctx); //deliberately run at full alpha for readability of overlay } private renderCommandSuggestions(ctx: CanvasRenderingContext2D) { @@ -625,7 +625,7 @@ export class ChatOverlay { if (w > maxWidth) maxWidth = w; } - ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'; + ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(0.85, SettingsManager.settings.chatOpacity)})`; ctx.fillRect(2, baseY - padding, maxWidth + padding * 2 + 2, lineHeight * maxVisible + padding * 2); for (let i = 0; i < maxVisible; i++) { From a68ac5ab14b0da5ff00c54cd9a6374ee988f7dcd Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:58:06 -0500 Subject: [PATCH 19/20] chore: delta time cap --- src/client/core/Game.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/core/Game.ts b/src/client/core/Game.ts index c0213298..816bc559 100644 --- a/src/client/core/Game.ts +++ b/src/client/core/Game.ts @@ -76,7 +76,7 @@ export class Game { animate() { if (this.stopped) return; - const deltaTime = this.clock.getDelta(); + const deltaTime = Math.min(this.clock.getDelta(), 1 / 5); // Basic frame profiler when enabled const profilerEnabled = SettingsManager.settings.profilerMode; From fe1e4adc6ad0aca7991cff38f228981a480e34aa Mon Sep 17 00:00:00 2001 From: Isaac Thoman <49598528+IsaacThoman@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:27:34 -0500 Subject: [PATCH 20/20] fix: still use default candiru.xyz peer even if unable to write to disk --- src/server/managers/PeerManager.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/server/managers/PeerManager.ts b/src/server/managers/PeerManager.ts index 3e6770c5..370e938f 100644 --- a/src/server/managers/PeerManager.ts +++ b/src/server/managers/PeerManager.ts @@ -89,8 +89,8 @@ export class PeerManager { } catch (error) { if (error instanceof Deno.errors.NotFound) { console.log(`${this.serversFilePath} not found. Creating with default.`); + const defaultUrl = 'https://candiru.xyz'; // Example default try { - const defaultUrl = 'https://candiru.xyz'; // Example default await Deno.writeTextFile(this.serversFilePath, `${defaultUrl}\n`); if (defaultUrl !== config.server.url) { this.updateQueue = [defaultUrl]; @@ -99,7 +99,13 @@ export class PeerManager { } } catch (writeError) { console.error(`Failed to create ${this.serversFilePath}:`, writeError); - this.updateQueue = []; // Start with empty queue if file creation fails + // Still use the default URL even if file write fails + if (defaultUrl !== config.server.url) { + this.updateQueue = [defaultUrl]; + console.log(`Using in-memory default peer: ${defaultUrl}`); + } else { + this.updateQueue = []; + } } } else { console.error(`Failed to read ${this.serversFilePath}:`, error);