├── Dockerfile ├── README.md ├── build.sh ├── client ├── .assetpack.js ├── .dockerignore ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── assets │ ├── apocalypseWallpaper.png │ ├── blood.png │ ├── dogtag.png │ ├── editor │ │ ├── box.jpg │ │ ├── missing.png │ │ └── prototype.png │ ├── player │ │ ├── texture-0.json │ │ ├── texture-0.png │ │ ├── texture-1.json │ │ ├── texture-1.png │ │ ├── texture-2.json │ │ └── texture-2.png │ ├── sand.jpg │ ├── sandwall.jpeg │ ├── sounds │ │ ├── coins │ │ │ ├── Coins_Single_00.mp3 │ │ │ ├── Coins_Single_01.mp3 │ │ │ ├── Coins_Single_02.mp3 │ │ │ ├── Coins_Single_03.mp3 │ │ │ ├── Coins_Single_04.mp3 │ │ │ ├── Coins_Single_05.mp3 │ │ │ ├── Coins_Single_06.mp3 │ │ │ ├── Coins_Single_07.mp3 │ │ │ ├── Coins_Single_08.mp3 │ │ │ ├── Coins_Single_09.mp3 │ │ │ ├── Coins_Single_10.mp3 │ │ │ ├── Coins_Single_11.mp3 │ │ │ ├── Coins_Single_12.mp3 │ │ │ ├── Coins_Single_13.mp3 │ │ │ ├── Coins_Single_14.mp3 │ │ │ ├── Coins_Single_15.mp3 │ │ │ ├── Coins_Single_16.mp3 │ │ │ ├── Coins_Single_17.mp3 │ │ │ ├── Coins_Single_18.mp3 │ │ │ ├── Coins_Single_19.mp3 │ │ │ ├── Coins_Single_20.mp3 │ │ │ ├── Coins_Single_21.mp3 │ │ │ ├── Coins_Single_22.mp3 │ │ │ ├── Coins_Single_23.mp3 │ │ │ ├── Coins_Single_24.mp3 │ │ │ ├── Coins_Single_25.mp3 │ │ │ ├── Coins_Single_26.mp3 │ │ │ ├── Coins_Single_27.mp3 │ │ │ ├── Coins_Single_28.mp3 │ │ │ ├── Coins_Single_29.mp3 │ │ │ ├── Coins_Single_30.mp3 │ │ │ ├── Coins_Single_31.mp3 │ │ │ ├── Coins_Single_32.mp3 │ │ │ ├── Coins_Single_33.mp3 │ │ │ ├── Coins_Single_34.mp3 │ │ │ ├── Coins_Single_35.mp3 │ │ │ ├── Coins_Single_36.mp3 │ │ │ ├── Coins_Single_37.mp3 │ │ │ ├── Coins_Single_38.mp3 │ │ │ ├── Coins_Single_39.mp3 │ │ │ ├── Coins_Single_40.mp3 │ │ │ ├── Coins_Single_41.mp3 │ │ │ ├── Coins_Single_42.mp3 │ │ │ ├── Coins_Single_43.mp3 │ │ │ ├── Coins_Single_44.mp3 │ │ │ ├── Coins_Single_45.mp3 │ │ │ ├── Coins_Single_46.mp3 │ │ │ ├── Coins_Single_47.mp3 │ │ │ ├── Coins_Single_48.mp3 │ │ │ ├── Coins_Single_49.mp3 │ │ │ ├── Coins_Single_50.mp3 │ │ │ ├── Coins_Single_51.mp3 │ │ │ ├── Coins_Single_52.mp3 │ │ │ ├── Coins_Single_53.mp3 │ │ │ ├── Coins_Single_54.mp3 │ │ │ └── Coins_Single_55.mp3 │ │ ├── growl1.wav │ │ ├── growl2.wav │ │ ├── growls │ │ │ ├── growl1.wav │ │ │ ├── growl2.wav │ │ │ └── monster │ │ │ │ ├── monster.1.ogg │ │ │ │ ├── monster.10.ogg │ │ │ │ ├── monster.11.ogg │ │ │ │ ├── monster.12.ogg │ │ │ │ ├── monster.13.ogg │ │ │ │ ├── monster.14.ogg │ │ │ │ ├── monster.15.ogg │ │ │ │ ├── monster.16.ogg │ │ │ │ ├── monster.2.ogg │ │ │ │ ├── monster.3.ogg │ │ │ │ ├── monster.4.ogg │ │ │ │ ├── monster.5.ogg │ │ │ │ ├── monster.6.ogg │ │ │ │ ├── monster.7.ogg │ │ │ │ ├── monster.8.ogg │ │ │ │ └── monster.9.ogg │ │ ├── guns │ │ │ ├── 9mm.mp3 │ │ │ ├── rifle.mp3 │ │ │ ├── shotgun.mp3 │ │ │ └── shotgun_rack.mp3 │ │ ├── hurt.ogg │ │ ├── impact.ogg │ │ ├── monster │ │ │ ├── monster.1.ogg │ │ │ ├── monster.10.ogg │ │ │ ├── monster.11.ogg │ │ │ ├── monster.12.ogg │ │ │ ├── monster.13.ogg │ │ │ ├── monster.14.ogg │ │ │ ├── monster.15.ogg │ │ │ ├── monster.16.ogg │ │ │ ├── monster.2.ogg │ │ │ ├── monster.3.ogg │ │ │ ├── monster.4.ogg │ │ │ ├── monster.5.ogg │ │ │ ├── monster.6.ogg │ │ │ ├── monster.7.ogg │ │ │ ├── monster.8.ogg │ │ │ └── monster.9.ogg │ │ ├── playerdies.mp3 │ │ ├── positive.ogg │ │ ├── punch.mp3 │ │ ├── punchMiss.mp3 │ │ ├── shot.wav │ │ ├── splat.ogg │ │ ├── waveStart.wav │ │ ├── zombieBig.wav │ │ └── zombieDeath.wav │ ├── ui │ │ └── skillPoint.png │ └── zombie │ │ └── zombie.png ├── bun.lockb ├── index.html ├── package.json ├── postcss.config.js ├── public │ ├── assets │ │ ├── apocalypseWallpaper.png │ │ ├── blood.png │ │ ├── dogtag.png │ │ ├── editor │ │ │ ├── box.jpg │ │ │ ├── missing.png │ │ │ └── prototype.png │ │ ├── manifest.json │ │ ├── player │ │ │ ├── texture-0.json │ │ │ ├── texture-0.png │ │ │ ├── texture-1.json │ │ │ ├── texture-1.png │ │ │ ├── texture-2.json │ │ │ └── texture-2.png │ │ ├── sand.jpg │ │ ├── sandwall.jpeg │ │ ├── sounds │ │ │ ├── coins │ │ │ │ ├── Coins_Single_00.mp3 │ │ │ │ ├── Coins_Single_01.mp3 │ │ │ │ ├── Coins_Single_02.mp3 │ │ │ │ ├── Coins_Single_03.mp3 │ │ │ │ ├── Coins_Single_04.mp3 │ │ │ │ ├── Coins_Single_05.mp3 │ │ │ │ ├── Coins_Single_06.mp3 │ │ │ │ ├── Coins_Single_07.mp3 │ │ │ │ ├── Coins_Single_08.mp3 │ │ │ │ ├── Coins_Single_09.mp3 │ │ │ │ ├── Coins_Single_10.mp3 │ │ │ │ ├── Coins_Single_11.mp3 │ │ │ │ ├── Coins_Single_12.mp3 │ │ │ │ ├── Coins_Single_13.mp3 │ │ │ │ ├── Coins_Single_14.mp3 │ │ │ │ ├── Coins_Single_15.mp3 │ │ │ │ ├── Coins_Single_16.mp3 │ │ │ │ ├── Coins_Single_17.mp3 │ │ │ │ ├── Coins_Single_18.mp3 │ │ │ │ ├── Coins_Single_19.mp3 │ │ │ │ ├── Coins_Single_20.mp3 │ │ │ │ ├── Coins_Single_21.mp3 │ │ │ │ ├── Coins_Single_22.mp3 │ │ │ │ ├── Coins_Single_23.mp3 │ │ │ │ ├── Coins_Single_24.mp3 │ │ │ │ ├── Coins_Single_25.mp3 │ │ │ │ ├── Coins_Single_26.mp3 │ │ │ │ ├── Coins_Single_27.mp3 │ │ │ │ ├── Coins_Single_28.mp3 │ │ │ │ ├── Coins_Single_29.mp3 │ │ │ │ ├── Coins_Single_30.mp3 │ │ │ │ ├── Coins_Single_31.mp3 │ │ │ │ ├── Coins_Single_32.mp3 │ │ │ │ ├── Coins_Single_33.mp3 │ │ │ │ ├── Coins_Single_34.mp3 │ │ │ │ ├── Coins_Single_35.mp3 │ │ │ │ ├── Coins_Single_36.mp3 │ │ │ │ ├── Coins_Single_37.mp3 │ │ │ │ ├── Coins_Single_38.mp3 │ │ │ │ ├── Coins_Single_39.mp3 │ │ │ │ ├── Coins_Single_40.mp3 │ │ │ │ ├── Coins_Single_41.mp3 │ │ │ │ ├── Coins_Single_42.mp3 │ │ │ │ ├── Coins_Single_43.mp3 │ │ │ │ ├── Coins_Single_44.mp3 │ │ │ │ ├── Coins_Single_45.mp3 │ │ │ │ ├── Coins_Single_46.mp3 │ │ │ │ ├── Coins_Single_47.mp3 │ │ │ │ ├── Coins_Single_48.mp3 │ │ │ │ ├── Coins_Single_49.mp3 │ │ │ │ ├── Coins_Single_50.mp3 │ │ │ │ ├── Coins_Single_51.mp3 │ │ │ │ ├── Coins_Single_52.mp3 │ │ │ │ ├── Coins_Single_53.mp3 │ │ │ │ ├── Coins_Single_54.mp3 │ │ │ │ └── Coins_Single_55.mp3 │ │ │ ├── growl1.wav │ │ │ ├── growl2.wav │ │ │ ├── growls │ │ │ │ ├── growl1.wav │ │ │ │ ├── growl2.wav │ │ │ │ └── monster │ │ │ │ │ ├── monster.1.ogg │ │ │ │ │ ├── monster.10.ogg │ │ │ │ │ ├── monster.11.ogg │ │ │ │ │ ├── monster.12.ogg │ │ │ │ │ ├── monster.13.ogg │ │ │ │ │ ├── monster.14.ogg │ │ │ │ │ ├── monster.15.ogg │ │ │ │ │ ├── monster.16.ogg │ │ │ │ │ ├── monster.2.ogg │ │ │ │ │ ├── monster.3.ogg │ │ │ │ │ ├── monster.4.ogg │ │ │ │ │ ├── monster.5.ogg │ │ │ │ │ ├── monster.6.ogg │ │ │ │ │ ├── monster.7.ogg │ │ │ │ │ ├── monster.8.ogg │ │ │ │ │ └── monster.9.ogg │ │ │ ├── guns │ │ │ │ ├── 9mm.mp3 │ │ │ │ ├── rifle.mp3 │ │ │ │ ├── shotgun.mp3 │ │ │ │ └── shotgun_rack.mp3 │ │ │ ├── hurt.ogg │ │ │ ├── impact.ogg │ │ │ ├── monster │ │ │ │ ├── monster.1.ogg │ │ │ │ ├── monster.10.ogg │ │ │ │ ├── monster.11.ogg │ │ │ │ ├── monster.12.ogg │ │ │ │ ├── monster.13.ogg │ │ │ │ ├── monster.14.ogg │ │ │ │ ├── monster.15.ogg │ │ │ │ ├── monster.16.ogg │ │ │ │ ├── monster.2.ogg │ │ │ │ ├── monster.3.ogg │ │ │ │ ├── monster.4.ogg │ │ │ │ ├── monster.5.ogg │ │ │ │ ├── monster.6.ogg │ │ │ │ ├── monster.7.ogg │ │ │ │ ├── monster.8.ogg │ │ │ │ └── monster.9.ogg │ │ │ ├── playerdies.mp3 │ │ │ ├── positive.ogg │ │ │ ├── punch.mp3 │ │ │ ├── punchMiss.mp3 │ │ │ ├── shot.wav │ │ │ ├── splat.ogg │ │ │ ├── waveStart.wav │ │ │ ├── zombieBig.wav │ │ │ └── zombieDeath.wav │ │ ├── ui │ │ │ └── skillPoint.png │ │ └── zombie │ │ │ └── zombie.png │ └── favicon.ico ├── src │ ├── App.tsx │ ├── assets │ │ ├── assetHandler.ts │ │ └── spritesheets │ │ │ ├── playerAnimationAtlas.ts │ │ │ └── zombie.ts │ ├── colyseus.ts │ ├── components │ │ ├── HealthBar.tsx │ │ ├── MainStage.tsx │ │ ├── Wall.tsx │ │ ├── bullets │ │ │ ├── Bullets.tsx │ │ │ └── bullet.ts │ │ ├── coins │ │ │ └── coinLogic.ts │ │ ├── effects │ │ │ └── Blood.tsx │ │ ├── graphics │ │ │ ├── Camera.tsx │ │ │ ├── FullScreenStage.tsx │ │ │ ├── MyAnimatedSprite.tsx │ │ │ ├── cameraStore.ts │ │ │ └── filters.ts │ │ ├── level │ │ │ ├── AssetObjectInstance.tsx │ │ │ ├── LevelInstanceRenderer.tsx │ │ │ ├── levelContext.ts │ │ │ ├── spawnPointContext.ts │ │ │ └── useRemoteLevel.ts │ │ ├── player │ │ │ ├── GunManager.tsx │ │ │ ├── PlayerSelf.tsx │ │ │ ├── PlayerSpawner.tsx │ │ │ ├── PlayerSprite.tsx │ │ │ ├── Players.tsx │ │ │ └── SpectateControls.tsx │ │ ├── stageContext.ts │ │ ├── testlevel.json │ │ ├── ui │ │ │ ├── CharacterPreview.tsx │ │ │ ├── Chat.tsx │ │ │ ├── EscapeScreen.tsx │ │ │ ├── GameUI.tsx │ │ │ ├── JoinMenu.tsx │ │ │ ├── LeaderBoard.tsx │ │ │ ├── Menu.tsx │ │ │ ├── UpgradeStore.tsx │ │ │ ├── characterCustomizationStore.ts │ │ │ ├── mainMenu │ │ │ │ ├── AuthSection.tsx │ │ │ │ └── MapSelector.tsx │ │ │ ├── soundStore.ts │ │ │ ├── uiStore.ts │ │ │ └── uiUtils.tsx │ │ ├── util │ │ │ ├── FpsDisplay.tsx │ │ │ ├── Spinner.tsx │ │ │ └── clientCommands.ts │ │ └── zombies │ │ │ ├── MyZombie.tsx │ │ │ ├── ZombieSpawner.tsx │ │ │ ├── Zombies.tsx │ │ │ ├── pathfinding │ │ │ ├── astar.ts │ │ │ └── grid.ts │ │ │ ├── zombieColliders.ts │ │ │ ├── zombieHooks.ts │ │ │ └── zombieLogic.ts │ ├── editor │ │ ├── AssetLibrary.tsx │ │ ├── CreateAssetsUI.tsx │ │ ├── EditorCamera.tsx │ │ ├── FilesUI.tsx │ │ ├── LevelObject.tsx │ │ ├── MapEditor.tsx │ │ ├── MapEditorUI.tsx │ │ ├── VisualColliders.tsx │ │ ├── assets │ │ │ └── hooks.ts │ │ └── mapEditorStore.ts │ ├── index.css │ ├── lib │ │ ├── auth │ │ │ ├── colyseusAuth.ts │ │ │ └── logto.ts │ │ ├── gameStore.ts │ │ ├── graphics │ │ │ └── useAsset.ts │ │ ├── hooks │ │ │ └── usePlayers.ts │ │ ├── networking │ │ │ ├── batches.ts │ │ │ ├── hooks.ts │ │ │ └── rooms.ts │ │ ├── physics │ │ │ ├── PhysicsProvider.tsx │ │ │ ├── context.ts │ │ │ ├── hooks.ts │ │ │ └── ticker.tsx │ │ ├── sound │ │ │ └── sound.ts │ │ ├── trpc │ │ │ ├── TrpcWrapper.tsx │ │ │ ├── backendUrl.tsx │ │ │ └── trpcClient.ts │ │ ├── useControls.ts │ │ ├── useLerped.ts │ │ └── useRerender.ts │ ├── main.tsx │ └── routes │ │ └── callback.tsx ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── credits.md ├── dev.sh ├── server ├── .dockerignore ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── bun.lockb ├── ecosystem.config.js ├── loadtest │ └── example.ts ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240423202723_init │ │ │ └── migration.sql │ │ ├── 20240425202946_add_maps │ │ │ └── migration.sql │ │ ├── 20240426162156_permissions_and_more_map_info │ │ │ └── migration.sql │ │ ├── 20240428115223_add_custom_assets │ │ │ └── migration.sql │ │ ├── 20240429115754_add_played_games │ │ │ └── migration.sql │ │ ├── 20240429152723_add_username_to_participant │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── app.config.ts │ ├── game │ │ ├── WaveManager.ts │ │ ├── config.ts │ │ ├── console │ │ │ └── commandHandler.ts │ │ ├── mapEditor │ │ │ └── editorTypes.ts │ │ ├── maps.ts │ │ ├── player.ts │ │ ├── waves.ts │ │ └── zombies.ts │ ├── index.ts │ ├── prisma.ts │ ├── rooms │ │ ├── MyRoom.ts │ │ └── schema │ │ │ └── MyRoomState.ts │ ├── trpc │ │ ├── assetRouter.ts │ │ ├── context.ts │ │ ├── mapRouter.ts │ │ ├── router.ts │ │ ├── statRouter.ts │ │ └── trpc.ts │ └── util.ts ├── test │ └── MyRoom_test.ts └── tsconfig.json └── start.sh /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM imbios/bun-node as base 2 | 3 | # install server 4 | WORKDIR /app/server 5 | 6 | COPY ./server/package.json ./server/bun.lockb ./ 7 | RUN bun install 8 | 9 | FROM base as client 10 | 11 | WORKDIR /app/client 12 | 13 | COPY ./client/package.json ./client/bun.lockb ./ 14 | RUN bun install 15 | 16 | COPY ./client ./ 17 | # COPY server source from ./server/src to ../server/src 18 | COPY ./server/src ../server/src 19 | 20 | RUN bun run build 21 | 22 | FROM base as server 23 | 24 | WORKDIR /app/server 25 | 26 | COPY ./server ./ 27 | RUN bunx prisma generate 28 | RUN bun run build 29 | 30 | COPY --from=client /app/client/dist ./client/dist 31 | 32 | ENV PORT=3000 33 | 34 | EXPOSE 3000 35 | 36 | ENV NODE_ENV=production 37 | 38 | # copy and permit start.sh 39 | COPY ./start.sh ./ 40 | RUN chmod +x ./start.sh 41 | 42 | CMD ["sh", "-c", "./start.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zombies Multiplayer 2 | This is a simple multiplayer game where you fight against increasing waves of zombies together with your friends. 3 | 4 | ## Come check it out! 5 | 6 | The game ist hosted on [apocalypse.p3ntest.dev](https://apocalypse.p3ntest.dev/). 7 | 8 | And please report any bugs or feature requests right back to us. 9 | 10 | 11 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | bunx concurrently "cd client && bun run build" "cd server && bun run build" -------------------------------------------------------------------------------- /client/.assetpack.js: -------------------------------------------------------------------------------- 1 | import { compressJpg, compressPng } from "@assetpack/plugin-compress"; 2 | import { pixiManifest } from "@assetpack/plugin-manifest"; 3 | 4 | export default { 5 | entry: "./assets", 6 | output: "./public/assets", 7 | plugins: { 8 | compressJpg: compressJpg(), 9 | compressPng: compressPng(), 10 | manifest: pixiManifest(), 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh", "unused-imports"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | "unused-imports/no-unused-imports": "error", 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /client/assets/apocalypseWallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/apocalypseWallpaper.png -------------------------------------------------------------------------------- /client/assets/blood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/blood.png -------------------------------------------------------------------------------- /client/assets/dogtag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/dogtag.png -------------------------------------------------------------------------------- /client/assets/editor/box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/editor/box.jpg -------------------------------------------------------------------------------- /client/assets/editor/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/editor/missing.png -------------------------------------------------------------------------------- /client/assets/editor/prototype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/editor/prototype.png -------------------------------------------------------------------------------- /client/assets/player/texture-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/player/texture-0.png -------------------------------------------------------------------------------- /client/assets/player/texture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/player/texture-1.png -------------------------------------------------------------------------------- /client/assets/player/texture-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/player/texture-2.png -------------------------------------------------------------------------------- /client/assets/sand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sand.jpg -------------------------------------------------------------------------------- /client/assets/sandwall.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sandwall.jpeg -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_00.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_00.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_01.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_02.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_03.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_03.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_04.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_04.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_05.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_05.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_06.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_06.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_07.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_07.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_08.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_08.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_09.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_09.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_10.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_10.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_11.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_11.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_12.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_12.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_13.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_13.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_14.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_14.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_15.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_15.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_16.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_16.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_17.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_17.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_18.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_18.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_19.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_19.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_20.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_20.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_21.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_21.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_22.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_22.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_23.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_23.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_24.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_24.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_25.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_25.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_26.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_26.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_27.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_27.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_28.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_28.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_29.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_29.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_30.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_30.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_31.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_31.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_32.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_32.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_33.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_33.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_34.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_34.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_35.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_35.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_36.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_36.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_37.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_37.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_38.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_38.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_39.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_39.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_40.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_40.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_41.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_41.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_42.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_42.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_43.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_43.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_44.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_44.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_45.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_45.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_46.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_46.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_47.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_47.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_48.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_48.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_49.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_49.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_50.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_50.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_51.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_51.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_52.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_52.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_53.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_53.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_54.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_54.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/coins/Coins_Single_55.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/coins/Coins_Single_55.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/growl1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growl1.wav -------------------------------------------------------------------------------- /client/assets/sounds/growl2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growl2.wav -------------------------------------------------------------------------------- /client/assets/sounds/growls/growl1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/growl1.wav -------------------------------------------------------------------------------- /client/assets/sounds/growls/growl2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/growl2.wav -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.1.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.10.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.10.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.11.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.11.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.12.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.12.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.13.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.13.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.14.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.14.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.15.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.15.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.16.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.16.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.2.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.3.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.4.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.5.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.5.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.6.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.6.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.7.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.7.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.8.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.8.ogg -------------------------------------------------------------------------------- /client/assets/sounds/growls/monster/monster.9.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/growls/monster/monster.9.ogg -------------------------------------------------------------------------------- /client/assets/sounds/guns/9mm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/guns/9mm.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/guns/rifle.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/guns/rifle.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/guns/shotgun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/guns/shotgun.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/guns/shotgun_rack.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/guns/shotgun_rack.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/hurt.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/hurt.ogg -------------------------------------------------------------------------------- /client/assets/sounds/impact.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/impact.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.1.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.10.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.10.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.11.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.11.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.12.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.12.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.13.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.13.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.14.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.14.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.15.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.15.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.16.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.16.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.2.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.3.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.4.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.5.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.5.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.6.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.6.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.7.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.7.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.8.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.8.ogg -------------------------------------------------------------------------------- /client/assets/sounds/monster/monster.9.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/monster/monster.9.ogg -------------------------------------------------------------------------------- /client/assets/sounds/playerdies.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/playerdies.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/positive.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/positive.ogg -------------------------------------------------------------------------------- /client/assets/sounds/punch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/punch.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/punchMiss.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/punchMiss.mp3 -------------------------------------------------------------------------------- /client/assets/sounds/shot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/shot.wav -------------------------------------------------------------------------------- /client/assets/sounds/splat.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/splat.ogg -------------------------------------------------------------------------------- /client/assets/sounds/waveStart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/waveStart.wav -------------------------------------------------------------------------------- /client/assets/sounds/zombieBig.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/zombieBig.wav -------------------------------------------------------------------------------- /client/assets/sounds/zombieDeath.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/sounds/zombieDeath.wav -------------------------------------------------------------------------------- /client/assets/ui/skillPoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/ui/skillPoint.png -------------------------------------------------------------------------------- /client/assets/zombie/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/assets/zombie/zombie.png -------------------------------------------------------------------------------- /client/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/bun.lockb -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Zombies Multiplayer 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zombies-multiplayer", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --fix", 10 | "preview": "vite preview", 11 | "build:assets": "assetpack -c .assetpack.js" 12 | }, 13 | "dependencies": { 14 | "@logto/react": "^3.0.8", 15 | "@p3ntest/use-colyseus": "^0.0.9", 16 | "@pixi/events": "^7.4.2", 17 | "@pixi/react": "^7.1.2", 18 | "@pixi/sound": "5.2.3", 19 | "@tanstack/react-query": "latest", 20 | "@trpc/client": "next", 21 | "@trpc/react-query": "next", 22 | "express": "^4.19.2", 23 | "immer": "^10.0.4", 24 | "lodash": "^4.17.21", 25 | "matter-js": "^0.19.0", 26 | "offset-polygon": "^0.9.2", 27 | "pathfinding": "^0.4.18", 28 | "pixi-filters": "5.3.0", 29 | "pixi.js": "^7.4.2", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-joystick-component": "^6.2.1", 33 | "react-router-dom": "^6.23.0", 34 | "tailwind-merge": "^2.3.0", 35 | "usehooks-ts": "^3.1.0", 36 | "zod": "^3.23.4", 37 | "zustand": "^4.5.2" 38 | }, 39 | "devDependencies": { 40 | "@assetpack/cli": "^0.8.0", 41 | "@assetpack/core": "^0.8.0", 42 | "@assetpack/plugin-compress": "^0.8.0", 43 | "@assetpack/plugin-manifest": "^0.8.0", 44 | "@types/express": "^4.17.21", 45 | "@types/lodash": "^4.17.0", 46 | "@types/matter-js": "^0.19.6", 47 | "@types/pathfinding": "^0.0.9", 48 | "@types/react": "^18.2.66", 49 | "@types/react-dom": "^18.2.22", 50 | "@typescript-eslint/eslint-plugin": "^7.2.0", 51 | "@typescript-eslint/parser": "^7.2.0", 52 | "@vitejs/plugin-react": "^4.2.1", 53 | "autoprefixer": "^10.4.19", 54 | "daisyui": "latest", 55 | "eslint": "^8.57.0", 56 | "eslint-plugin-react-hooks": "^4.6.0", 57 | "eslint-plugin-react-refresh": "^0.4.6", 58 | "eslint-plugin-unused-imports": "^3.1.0", 59 | "postcss": "^8.4.38", 60 | "tailwindcss": "^3.4.3", 61 | "typescript": "^5.2.2", 62 | "vite": "^5.2.0" 63 | } 64 | } -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/public/assets/apocalypseWallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/apocalypseWallpaper.png -------------------------------------------------------------------------------- /client/public/assets/blood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/blood.png -------------------------------------------------------------------------------- /client/public/assets/dogtag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/dogtag.png -------------------------------------------------------------------------------- /client/public/assets/editor/box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/editor/box.jpg -------------------------------------------------------------------------------- /client/public/assets/editor/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/editor/missing.png -------------------------------------------------------------------------------- /client/public/assets/editor/prototype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/editor/prototype.png -------------------------------------------------------------------------------- /client/public/assets/player/texture-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/player/texture-0.png -------------------------------------------------------------------------------- /client/public/assets/player/texture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/player/texture-1.png -------------------------------------------------------------------------------- /client/public/assets/player/texture-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/player/texture-2.png -------------------------------------------------------------------------------- /client/public/assets/sand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sand.jpg -------------------------------------------------------------------------------- /client/public/assets/sandwall.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sandwall.jpeg -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_00.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_00.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_01.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_02.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_03.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_03.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_04.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_04.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_05.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_05.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_06.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_06.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_07.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_07.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_08.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_08.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_09.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_09.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_10.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_10.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_11.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_11.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_12.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_12.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_13.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_13.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_14.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_14.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_15.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_15.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_16.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_16.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_17.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_17.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_18.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_18.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_19.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_19.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_20.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_20.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_21.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_21.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_22.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_22.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_23.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_23.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_24.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_24.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_25.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_25.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_26.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_26.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_27.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_27.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_28.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_28.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_29.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_29.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_30.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_30.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_31.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_31.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_32.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_32.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_33.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_33.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_34.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_34.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_35.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_35.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_36.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_36.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_37.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_37.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_38.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_38.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_39.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_39.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_40.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_40.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_41.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_41.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_42.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_42.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_43.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_43.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_44.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_44.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_45.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_45.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_46.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_46.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_47.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_47.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_48.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_48.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_49.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_49.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_50.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_50.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_51.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_51.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_52.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_52.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_53.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_53.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_54.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_54.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/coins/Coins_Single_55.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/coins/Coins_Single_55.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/growl1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growl1.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/growl2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growl2.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/growl1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/growl1.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/growl2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/growl2.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.1.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.10.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.10.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.11.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.11.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.12.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.12.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.13.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.13.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.14.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.14.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.15.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.15.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.16.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.16.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.2.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.3.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.4.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.5.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.5.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.6.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.6.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.7.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.7.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.8.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.8.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/growls/monster/monster.9.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/growls/monster/monster.9.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/guns/9mm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/guns/9mm.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/guns/rifle.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/guns/rifle.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/guns/shotgun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/guns/shotgun.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/guns/shotgun_rack.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/guns/shotgun_rack.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/hurt.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/hurt.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/impact.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/impact.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.1.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.10.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.10.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.11.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.11.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.12.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.12.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.13.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.13.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.14.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.14.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.15.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.15.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.16.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.16.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.2.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.3.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.4.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.5.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.5.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.6.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.6.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.7.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.7.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.8.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.8.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/monster/monster.9.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/monster/monster.9.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/playerdies.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/playerdies.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/positive.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/positive.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/punch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/punch.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/punchMiss.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/punchMiss.mp3 -------------------------------------------------------------------------------- /client/public/assets/sounds/shot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/shot.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/splat.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/splat.ogg -------------------------------------------------------------------------------- /client/public/assets/sounds/waveStart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/waveStart.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/zombieBig.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/zombieBig.wav -------------------------------------------------------------------------------- /client/public/assets/sounds/zombieDeath.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/sounds/zombieDeath.wav -------------------------------------------------------------------------------- /client/public/assets/ui/skillPoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/ui/skillPoint.png -------------------------------------------------------------------------------- /client/public/assets/zombie/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/assets/zombie/zombie.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useColyseusRoom } from "./colyseus"; 2 | import { MainStage } from "./components/MainStage"; 3 | import { Menu } from "./components/ui/Menu"; 4 | import { useTryJoinByQueryOrReconnectToken } from "./lib/networking/hooks"; 5 | import { useAssetStore, useEnsureAssetsLoaded } from "./assets/assetHandler"; 6 | import { Spinner } from "./components/util/Spinner"; 7 | import { LogtoProvider } from "@logto/react"; 8 | import { logtoConfig } from "./lib/auth/logto"; 9 | import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; 10 | import { CallBackHandler } from "./routes/callback"; 11 | import { MapEditor } from "./editor/MapEditor"; 12 | import { TrpcWrapper } from "./lib/trpc/TrpcWrapper"; 13 | import { useSetColyseusAuthToken } from "./lib/auth/colyseusAuth"; 14 | 15 | const router = createBrowserRouter([ 16 | { 17 | // path: "/", 18 | element: , 19 | children: [ 20 | { 21 | path: "/", 22 | element: , 23 | }, 24 | { 25 | path: "/editor", 26 | element: , 27 | }, 28 | ], 29 | }, 30 | { 31 | path: "/auth/callback", 32 | element: , 33 | }, 34 | ]); 35 | 36 | export function Router() { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function AssetLoadLayout() { 47 | useEnsureAssetsLoaded(); 48 | const { ready, isLoading } = useAssetStore(); 49 | 50 | return ( 51 | <> 52 | {isLoading && ( 53 |
54 | 55 |
56 | )} 57 | {ready && } 58 | 59 | ); 60 | } 61 | 62 | export function App() { 63 | useSetColyseusAuthToken(); 64 | return ; 65 | } 66 | 67 | function Game() { 68 | const room = useColyseusRoom(); 69 | useTryJoinByQueryOrReconnectToken(); 70 | 71 | if (!room) { 72 | return ; 73 | } 74 | 75 | return ( 76 |
81 | 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /client/src/assets/assetHandler.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { zombieAtlas } from "./spritesheets/zombie"; 3 | import { Assets, Spritesheet, Texture } from "pixi.js"; 4 | import { loadPlayerAnimationSprites } from "./spritesheets/playerAnimationAtlas"; 5 | import "@pixi/sound"; 6 | interface AssetStore { 7 | ready: boolean; 8 | setReady: (ready: boolean) => void; 9 | isLoading: boolean; 10 | setIsLoading: (isLoading: boolean) => void; 11 | } 12 | 13 | export const useAssetStore = create((set) => ({ 14 | ready: false, 15 | setReady: (ready: boolean) => set({ ready }), 16 | isLoading: false, 17 | setIsLoading: (isLoading: boolean) => set({ isLoading }), 18 | })); 19 | 20 | const atlasMap = { 21 | zombieAtlas, 22 | } as const; 23 | 24 | export const spriteSheets: Record = 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | {} as any; 27 | 28 | export function useEnsureAssetsLoaded() { 29 | const { ready, setReady, isLoading, setIsLoading } = useAssetStore(); 30 | 31 | if (!ready && !isLoading) { 32 | setIsLoading(true); 33 | loadAssets().then(() => { 34 | setReady(true); 35 | setIsLoading(false); 36 | }); 37 | } 38 | } 39 | 40 | const additionalResources = [ 41 | "/assets/player/texture-0.json", 42 | "/assets/player/texture-2.json", 43 | "/assets/player/texture-1.json", 44 | ]; 45 | 46 | async function loadAssets() { 47 | console.time("loadAssets"); 48 | await Assets.init({ 49 | manifest: "/assets/manifest.json", 50 | basePath: "/assets", 51 | }); 52 | await Assets.loadBundle("default"); 53 | // load all textures 54 | const textures = [ 55 | ...Object.values(atlasMap).map((atlas) => atlas.meta.image), 56 | ...additionalResources, 57 | ]; 58 | await Assets.load(textures); 59 | 60 | // load all spritesheets 61 | await Promise.all([ 62 | ...Object.entries(atlasMap).map(async ([key, atlas]) => { 63 | const sheet = new Spritesheet(Texture.from(atlas.meta.image!), atlas); 64 | await sheet.parse(); 65 | spriteSheets[key as keyof typeof atlasMap] = sheet; 66 | }), 67 | loadPlayerAnimationSprites(), 68 | ]); 69 | console.timeEnd("loadAssets"); 70 | } 71 | -------------------------------------------------------------------------------- /client/src/assets/spritesheets/zombie.ts: -------------------------------------------------------------------------------- 1 | import { SpriteSheetJson } from "pixi.js"; 2 | 3 | const numFrames = 17; 4 | const width = 4896; 5 | const widthPerFrame = width / numFrames; 6 | 7 | export const zombieAtlas: SpriteSheetJson = { 8 | meta: { 9 | image: "/assets/zombie/zombie.png", 10 | scale: "1", 11 | }, 12 | frames: Object.fromEntries( 13 | Array.from({ length: numFrames }).map((_, i) => [ 14 | `zombie-${i}`, 15 | { 16 | frame: { 17 | x: i * widthPerFrame, 18 | y: 0, 19 | w: widthPerFrame, 20 | h: 311, 21 | }, 22 | anchor: { 23 | x: 0.5, 24 | y: 0.5, 25 | }, 26 | rotated: false, 27 | trimmed: false, 28 | spriteSourceSize: { 29 | x: 0, 30 | y: 0, 31 | w: widthPerFrame, 32 | h: 311, 33 | }, 34 | sourceSize: { 35 | w: widthPerFrame, 36 | h: 311, 37 | }, 38 | } as SpriteSheetJson["frames"]["zombie-0"], 39 | ]) 40 | ), 41 | animations: { 42 | walk: Array.from({ length: numFrames }).map((_, i) => `zombie-${i}`), 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /client/src/colyseus.ts: -------------------------------------------------------------------------------- 1 | import { MyRoomState } from "../../server/src/rooms/schema/MyRoomState"; 2 | import { colyseus } from "@p3ntest/use-colyseus"; 3 | 4 | const productionWebsocketUrl = window.location.origin.replace(/^http/, "ws"); 5 | const websocketUrl = 6 | process.env.NODE_ENV !== "production" 7 | ? "ws://localhost:2567" 8 | : productionWebsocketUrl; 9 | 10 | const { 11 | client, 12 | connectToColyseus, 13 | disconnectFromColyseus, 14 | setCurrentRoom, 15 | useColyseusRoom, 16 | useColyseusState, 17 | } = colyseus(websocketUrl); 18 | 19 | export { 20 | client as colyseusClient, 21 | setCurrentRoom, 22 | connectToColyseus, 23 | disconnectFromColyseus, 24 | useColyseusRoom, 25 | useColyseusState, 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/components/HealthBar.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Graphics, Text } from "@pixi/react"; 2 | import type { Graphics as G } from "@pixi/graphics"; 3 | import { useCallback } from "react"; 4 | import { TextStyle } from "pixi.js"; 5 | import { useLerped } from "../lib/useLerped"; 6 | 7 | export function HealthBar({ 8 | health, 9 | maxHealth = 100, 10 | }: { 11 | health: number; 12 | maxHealth: number; 13 | }) { 14 | const lerpedHealth = useLerped(health, 0.3); 15 | 16 | const draw = useCallback( 17 | (g: G) => { 18 | g.beginFill(0xff0000); 19 | g.drawRect(0, 0, 100, 10); 20 | g.endFill(); 21 | 22 | g.beginFill(0x00ff00); 23 | const healthRel = (lerpedHealth / maxHealth) * 100; 24 | g.drawRect(0, 0, healthRel, 10); 25 | g.endFill(); 26 | }, 27 | [lerpedHealth, maxHealth] 28 | ); 29 | 30 | if (health >= maxHealth) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 36 | 47 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /client/src/components/MainStage.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { useApp } from "@pixi/react"; 3 | import { Players } from "./player/Players"; 4 | import { useEffect } from "react"; 5 | import "@pixi/events"; 6 | import { PhysicsProvider } from "../lib/physics/PhysicsProvider"; 7 | import { Bullets } from "./bullets/Bullets"; 8 | import { Zombies } from "./zombies/Zombies"; 9 | import { 10 | useBroadcastRoomMessages, 11 | useSetQueryOrReconnectToken, 12 | } from "../lib/networking/hooks"; 13 | import { ZombieSpawner } from "./zombies/ZombieSpawner"; 14 | import { GameUI } from "./ui/GameUI"; 15 | import { BloodManager } from "./effects/Blood"; 16 | import { GameCamera } from "./graphics/Camera"; 17 | import { PlayerSpawner } from "./player/PlayerSpawner"; 18 | import { FullScreenStage } from "./graphics/FullScreenStage"; 19 | import { LevelInstanceRenderer } from "./level/LevelInstanceRenderer"; 20 | import { useCurrentRemoteLevel } from "./level/useRemoteLevel"; 21 | import { LevelProvider } from "./level/levelContext"; 22 | import { useControlEventListeners } from "../lib/useControls"; 23 | /** 24 | * This renders the actual in game content. It requires to be connected to a game room. 25 | * It will render the game world and all entities in it, as well as handle UI and Controls 26 | */ 27 | export const MainStage = () => { 28 | useBroadcastRoomMessages(); 29 | useControlEventListeners(); 30 | useSetQueryOrReconnectToken(); 31 | const level = useCurrentRemoteLevel()?.level ?? null; 32 | 33 | return ( 34 | <> 35 | 36 | 37 | 38 | {/* For some damn reason the camera must be here from the beginning */} 39 | 40 | {level && ( 41 | 42 | 43 | <> 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | )} 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export function Resizer() { 63 | const app = useApp(); 64 | useEffect(() => { 65 | const resize = () => { 66 | app.renderer.resize(window.innerWidth, window.innerHeight); 67 | }; 68 | window.addEventListener("resize", resize); 69 | return () => { 70 | window.removeEventListener("resize", resize); 71 | }; 72 | }, [app]); 73 | return null; 74 | } 75 | -------------------------------------------------------------------------------- /client/src/components/Wall.tsx: -------------------------------------------------------------------------------- 1 | import { Bodies } from "matter-js"; 2 | import { useBodyRef } from "../lib/physics/hooks"; 3 | import { TilingSprite } from "@pixi/react"; 4 | import { Texture } from "pixi.js"; 5 | 6 | export function Wall({ 7 | x, 8 | y, 9 | width, 10 | height, 11 | }: { 12 | x: number; 13 | y: number; 14 | width: number; 15 | height: number; 16 | }) { 17 | useBodyRef(() => Bodies.rectangle(x, y, width, height, { isStatic: true }), { 18 | tags: ["destroyBullet", "obstacle"], 19 | }); 20 | 21 | return ( 22 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /client/src/components/bullets/Bullets.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Graphics, useTick } from "@pixi/react"; 2 | import { useColyseusRoom, useColyseusState } from "../../colyseus"; 3 | import { BulletState } from "../../../../server/src/rooms/schema/MyRoomState"; 4 | import { useCallback, useState } from "react"; 5 | import { 6 | getBodyMeta, 7 | useBodyRef, 8 | useFilteredOnCollisionStart, 9 | } from "../../lib/physics/hooks"; 10 | import { Bodies, Body } from "matter-js"; 11 | import { useRerender } from "../../lib/useRerender"; 12 | import { bulletHitListeners } from "./bullet"; 13 | 14 | export function Bullets() { 15 | const state = useColyseusState(); 16 | const bullets = state?.bullets; 17 | 18 | return ( 19 | 20 | {bullets?.map((bullet) => ( 21 | 22 | ))} 23 | 24 | ); 25 | } 26 | 27 | function Bullet({ bullet }: { bullet: BulletState }) { 28 | const sessionId = useColyseusRoom()?.sessionId; 29 | const isMe = bullet.playerId === sessionId; 30 | 31 | if (isMe) { 32 | return ; 33 | } else { 34 | return ; 35 | } 36 | } 37 | 38 | function MyBullet({ bullet }: { bullet: BulletState }) { 39 | const room = useColyseusRoom(); 40 | const rerender = useRerender(); 41 | const [localDestroyed, setLocalDestroyed] = useState(false); // so the bullet doesn't go through the wall on the client 42 | 43 | const body = useBodyRef( 44 | () => Bodies.circle(bullet.originX, bullet.originY, 5, { isSensor: true }), 45 | { tags: ["bullet", "localBullet"] } 46 | ); 47 | 48 | const destroyBullet = useCallback(() => { 49 | if (localDestroyed) return; 50 | room?.send("destroyBullet", bullet.id); 51 | setLocalDestroyed(true); 52 | }, [room, bullet, localDestroyed]); 53 | 54 | useFilteredOnCollisionStart(body.current, (pair) => { 55 | const otherMeta = getBodyMeta(pair.bodyOther); 56 | if (otherMeta?.tags?.includes("destroyBullet")) { 57 | destroyBullet(); 58 | } 59 | if (bulletHitListeners.has(pair.bodyOther)) { 60 | for (const listener of bulletHitListeners.get(pair.bodyOther)!) { 61 | listener(bullet); 62 | } 63 | } 64 | }); 65 | 66 | useTick((delta) => { 67 | const dx = Math.cos(bullet.rotation) * bullet.speed * delta; 68 | const dy = Math.sin(bullet.rotation) * bullet.speed * delta; 69 | 70 | Body.translate(body.current, { x: dx, y: dy }); 71 | 72 | const x = body.current.position.x; 73 | const y = body.current.position.y; 74 | 75 | const distance = Math.sqrt( 76 | (x - bullet.originX) ** 2 + (y - bullet.originY) ** 2 77 | ); 78 | 79 | if (distance > 10000) { 80 | room?.send("destroyBullet", bullet.id); 81 | } 82 | 83 | rerender(); 84 | }); 85 | 86 | if (localDestroyed) return null; 87 | 88 | return ( 89 | 90 | ); 91 | } 92 | 93 | function OtherBullet({ bullet }: { bullet: BulletState }) { 94 | const [x, setX] = useState(bullet.originX); 95 | const [y, setY] = useState(bullet.originY); 96 | 97 | useTick((delta) => { 98 | const dx = Math.cos(bullet.rotation) * bullet.speed * delta; 99 | const dy = Math.sin(bullet.rotation) * bullet.speed * delta; 100 | 101 | setX((x) => x + dx); 102 | setY((y) => y + dy); 103 | }); 104 | 105 | return ; 106 | } 107 | 108 | function BulletGraphics({ x, y }: { x: number; y: number }) { 109 | return ( 110 | { 114 | g.beginFill(0x222); 115 | g.drawCircle(0, 0, 5); 116 | g.endFill(); 117 | }} 118 | /> 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /client/src/components/bullets/bullet.ts: -------------------------------------------------------------------------------- 1 | import { Body } from "matter-js"; 2 | import { BulletState } from "../../../../server/src/rooms/schema/MyRoomState"; 3 | import { useEffect, useRef } from "react"; 4 | 5 | type BulletHitCallback = (bullet: BulletState) => void; 6 | 7 | export const bulletHitListeners = new Map>(); 8 | 9 | export function useBulletHitListener(body: Body, callback: BulletHitCallback) { 10 | const currentCallback = useRef(callback); 11 | currentCallback.current = callback; 12 | 13 | useEffect(() => { 14 | const existingListeners = bulletHitListeners.get(body) ?? new Set(); 15 | existingListeners.add(callback); 16 | bulletHitListeners.set(body, existingListeners); 17 | 18 | return () => { 19 | const existingListeners = bulletHitListeners.get(body) ?? new Set(); 20 | existingListeners.delete(callback); 21 | bulletHitListeners.set(body, existingListeners); 22 | }; 23 | }, [body, callback]); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/coins/coinLogic.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/client/src/components/coins/coinLogic.ts -------------------------------------------------------------------------------- /client/src/components/effects/Blood.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useRoomMessageHandler } from "../../lib/networking/hooks"; 3 | import { ParticleContainer, Sprite, useTick } from "@pixi/react"; 4 | import { playSplat } from "../../lib/sound/sound"; 5 | import { Texture } from "pixi.js"; 6 | 7 | let currId = 0; 8 | 9 | export function BloodManager() { 10 | const [blood, setBlood] = useState< 11 | { 12 | x: number; 13 | y: number; 14 | size: number; 15 | id: number; 16 | }[] 17 | >([]); 18 | 19 | useRoomMessageHandler("blood", (message) => { 20 | const { x, y, size, amount = 1 } = message; 21 | for (let i = 0; i < amount; i++) 22 | setBlood((blood) => [ 23 | ...blood, 24 | { 25 | x, 26 | y, 27 | size, 28 | id: currId++, 29 | }, 30 | ]); 31 | playSplat(); 32 | }); 33 | 34 | return ( 35 | 36 | {blood.map((b) => ( 37 | { 43 | setBlood((blood) => blood.filter((bb) => bb.id !== b.id)); 44 | }} 45 | /> 46 | ))} 47 | {/* {}} /> */} 48 | 49 | ); 50 | } 51 | 52 | function Blood({ 53 | x, 54 | y, 55 | size, 56 | onRemove, 57 | }: { 58 | x: number; 59 | y: number; 60 | size: number; 61 | onRemove: () => void; 62 | }) { 63 | const [opacity, setOpacity] = useState(1); 64 | const [scale, setScale] = useState(0); 65 | const rotation = useState(Math.random() * Math.PI * 2)[0]; 66 | 67 | if (opacity <= 0) { 68 | onRemove(); 69 | } 70 | 71 | useTick((delta) => { 72 | setOpacity((o) => o - delta / 2000); 73 | setScale((s) => Math.min(1, s + delta / 8)); 74 | }, true); 75 | 76 | return ( 77 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /client/src/components/graphics/Camera.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useMemo, useRef } from "react"; 2 | import { useCameraStore } from "./cameraStore"; 3 | import { useWindowSize } from "usehooks-ts"; 4 | import { Container, useTick } from "@pixi/react"; 5 | import { CameraProvider } from "../stageContext"; 6 | import { Container as PIXIContainer } from "pixi.js"; 7 | 8 | export function GameCamera({ children }: { children: ReactNode }) { 9 | const windowSize = useWindowSize(); 10 | const screenSurface = windowSize.width * windowSize.height; 11 | const zoom = 12 | Math.sqrt(screenSurface / (1920 * 1080)) * 13 | 1.5 * 14 | useCameraStore((state) => state.zoom); 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | export function GenericCamera({ 24 | x, 25 | y, 26 | zoom, 27 | children, 28 | lerp = 0.04, 29 | }: { 30 | x: number; 31 | y: number; 32 | zoom: number; 33 | children: ReactNode; 34 | lerp?: number; 35 | }) { 36 | const windowSize = useWindowSize(); 37 | const scale = zoom; 38 | 39 | const camRef = useRef(null); 40 | 41 | const currentTarget = useRef({ x: 0, y: 0 }); 42 | const currentScale = useRef(1); 43 | const id = useMemo(() => Math.random().toString(26), []); 44 | 45 | useTick(() => { 46 | const cam = camRef.current; 47 | 48 | if (!cam) return; 49 | 50 | // stageRef?.levelContainer?.pivot.set(x, y); 51 | // stageRef?.levelContainer?.position.set(screen.width / 2, screen.height / 2); 52 | 53 | const LERP = lerp; 54 | 55 | currentTarget.current.x = 56 | currentTarget.current.x + (x - currentTarget.current.x) * LERP; 57 | currentTarget.current.y = 58 | currentTarget.current.y + (y - currentTarget.current.y) * LERP; 59 | 60 | currentScale.current = 61 | currentScale.current + (scale - currentScale.current) * LERP; 62 | 63 | cam.pivot.set(currentTarget.current.x, currentTarget.current.y); 64 | cam.position.set(windowSize.width / 2, windowSize.height / 2); 65 | cam.scale.set(currentScale.current); 66 | }); 67 | 68 | return ( 69 | 70 | 71 | {children} 72 | 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /client/src/components/graphics/FullScreenStage.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Stage, withFilters } from "@pixi/react"; 2 | import { useMemo } from "react"; 3 | import { useWindowSize } from "usehooks-ts"; 4 | import { TrpcWrapper } from "../../lib/trpc/TrpcWrapper"; 5 | import { logtoConfig } from "../../lib/auth/logto"; 6 | import { LogtoProvider } from "@logto/react"; 7 | import { useClientSettings } from "../ui/soundStore"; 8 | import { FpsTracker } from "../util/FpsDisplay"; 9 | import { filters } from "pixi.js"; 10 | 11 | // const Filters = withFilters(Container, { 12 | // shadows: new DropShadowFilter(), 13 | // }); 14 | 15 | export function FullScreenStage({ children }: { children: React.ReactNode }) { 16 | const windowSize = useWindowSize(); 17 | 18 | const options = useMemo(() => { 19 | return { 20 | background: "transparent", 21 | resolution: window.devicePixelRatio, 22 | eventMode: "static", 23 | } as const; 24 | }, []); 25 | 26 | const style = useMemo(() => { 27 | return { 28 | width: "100vw", 29 | height: "100vh", 30 | }; 31 | }, []); 32 | 33 | const showFps = useClientSettings((state) => state.showFps); 34 | 35 | return ( 36 | 0 ? windowSize.width : 800} 41 | height={ 42 | windowSize.height && windowSize.height > 0 ? windowSize.height : 600 43 | } 44 | > 45 | 46 | {children} 47 | 48 | {showFps && } 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /client/src/components/graphics/MyAnimatedSprite.tsx: -------------------------------------------------------------------------------- 1 | import { PixiComponent } from "@pixi/react"; 2 | import { AnimatedSprite } from "pixi.js"; 3 | 4 | export default PixiComponent("MyAnimatedSprite", { 5 | create: (props) => { 6 | return new AnimatedSprite(props.textures); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/components/graphics/cameraStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface CameraStore { 4 | x: number; 5 | y: number; 6 | zoom: number; 7 | setPosition(x: number, y: number): void; 8 | setZoom(zoom: number): void; 9 | } 10 | 11 | export const useCameraStore = create((set) => ({ 12 | x: 0, 13 | y: 0, 14 | zoom: 1, 15 | setPosition: (x, y) => set({ x, y }), 16 | setZoom: (zoom) => set({ zoom }), 17 | })); 18 | -------------------------------------------------------------------------------- /client/src/components/graphics/filters.ts: -------------------------------------------------------------------------------- 1 | import { DropShadowFilter } from "pixi-filters"; 2 | import { useMemo } from "react"; 3 | import { useLevelShadowSettings } from "../level/levelContext"; 4 | 5 | export function getEntityFilters() { 6 | return [ 7 | new DropShadowFilter({ 8 | blur: 0.2, 9 | quality: 0, 10 | alpha: 0.4, 11 | offset: { 12 | x: 7, 13 | y: 10, 14 | }, 15 | }), 16 | ]; 17 | } 18 | 19 | export function useEntityShadow() { 20 | const levelShadowSettings = useLevelShadowSettings(); 21 | 22 | return useMemo(() => { 23 | const offsetMagnitude = 1 * levelShadowSettings.sunOffset; 24 | const offsetX = 25 | Math.cos(levelShadowSettings.sunDirection) * offsetMagnitude; 26 | const offsetY = 27 | Math.sin(levelShadowSettings.sunDirection) * offsetMagnitude; 28 | 29 | return new DropShadowFilter({ 30 | blur: levelShadowSettings.shadowBlur + 1, 31 | alpha: levelShadowSettings.shadowAlpha, 32 | quality: 0, // needs to be 0 for the shadow to not bug around when moving 33 | offset: { 34 | x: offsetX, 35 | y: offsetY, 36 | }, 37 | }); 38 | }, [ 39 | levelShadowSettings.shadowAlpha, 40 | levelShadowSettings.shadowBlur, 41 | levelShadowSettings.sunDirection, 42 | levelShadowSettings.sunOffset, 43 | ]); 44 | } 45 | 46 | export function useLevelObjectShadow(objectHeight: number) { 47 | const levelShadowSettings = useLevelShadowSettings(); 48 | 49 | return useMemo(() => { 50 | const offsetMagnitude = objectHeight * levelShadowSettings.sunOffset; 51 | const offsetX = 52 | Math.cos(levelShadowSettings.sunDirection) * offsetMagnitude; 53 | const offsetY = 54 | Math.sin(levelShadowSettings.sunDirection) * offsetMagnitude; 55 | 56 | return new DropShadowFilter({ 57 | blur: levelShadowSettings.shadowBlur, 58 | alpha: levelShadowSettings.shadowAlpha, 59 | quality: 4, 60 | offset: { 61 | x: offsetX, 62 | y: offsetY, 63 | }, 64 | }); 65 | }, [ 66 | objectHeight, 67 | levelShadowSettings.shadowAlpha, 68 | levelShadowSettings.shadowBlur, 69 | levelShadowSettings.sunDirection, 70 | levelShadowSettings.sunOffset, 71 | ]); 72 | } 73 | -------------------------------------------------------------------------------- /client/src/components/level/levelContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useMemo } from "react"; 2 | import { GameLevel } from "../../../../server/src/game/mapEditor/editorTypes"; 3 | 4 | interface LevelContext { 5 | level: GameLevel; 6 | } 7 | 8 | const levelContext = createContext(null); 9 | export const LevelProvider = levelContext.Provider; 10 | 11 | export const useLevel = () => { 12 | const context = useContext(levelContext); 13 | if (!context) { 14 | throw new Error("useLevel must be used within a LevelProvider"); 15 | } 16 | return context; 17 | }; 18 | 19 | export function useLevelShadowSettings() { 20 | return useMemo(() => { 21 | return { 22 | sunOffset: 10, 23 | sunDirection: 1, 24 | shadowAlpha: 0.4, 25 | shadowBlur: 0.2, 26 | }; 27 | }, []); 28 | } 29 | 30 | export function usePaddedLevelBounds() { 31 | const { level } = useLevel(); 32 | 33 | return useMemo(() => { 34 | let minX = 0; 35 | let minY = 0; 36 | let maxX = 0; 37 | let maxY = 0; 38 | level.objects.forEach((object) => { 39 | minX = Math.min(minX, object.x); 40 | minY = Math.min(minY, object.y); 41 | maxX = Math.max(maxX, object.x); 42 | maxY = Math.max(maxY, object.y); 43 | }); 44 | 45 | const PADDING = 1000; 46 | return { 47 | minX: minX - PADDING, 48 | minY: minY - PADDING, 49 | maxX: maxX + PADDING, 50 | maxY: maxY + PADDING, 51 | }; 52 | }, [level.objects]); 53 | } 54 | -------------------------------------------------------------------------------- /client/src/components/level/spawnPointContext.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useLevel } from "./levelContext"; 3 | 4 | export function useSpawnPoints(type: "player" | "zombie") { 5 | const objects = useLevel().level.objects; 6 | 7 | const spawnPoints = useMemo(() => { 8 | return objects.filter( 9 | (object) => object.objectType === "spawnPoint" && object.spawns === type 10 | ); 11 | }, [objects, type]); 12 | 13 | return spawnPoints; 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/level/useRemoteLevel.ts: -------------------------------------------------------------------------------- 1 | import { trpc } from "../../lib/trpc/trpcClient"; 2 | import { useColyseusRoom, useColyseusState } from "../../colyseus"; 3 | import { useEffect } from "react"; 4 | 5 | export function useRemoteLevel(mapId: string | undefined) { 6 | const level = trpc.maps.loadMap.useQuery(mapId); 7 | 8 | if (!level.data) { 9 | return null; 10 | } 11 | 12 | return level.data; 13 | } 14 | 15 | export function useCurrentRemoteLevel() { 16 | const mapId = useColyseusState((state) => state.mapId); 17 | const room = useColyseusRoom(); 18 | const level = useRemoteLevel(mapId); 19 | 20 | useEffect(() => { 21 | if (level) { 22 | room.send("finishedLoading"); 23 | } 24 | }, [level, room]); 25 | 26 | return level; 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/player/PlayerSpawner.tsx: -------------------------------------------------------------------------------- 1 | import { useColyseusRoom } from "../../colyseus"; 2 | import { useRoomMessageHandler } from "../../lib/networking/hooks"; 3 | import { useSpawnPoints } from "../level/spawnPointContext"; 4 | 5 | export function PlayerSpawner() { 6 | const room = useColyseusRoom(); 7 | const spawnPoints = useSpawnPoints("player"); 8 | useRoomMessageHandler("requestSpawn", () => { 9 | const spawnPoint = 10 | spawnPoints[Math.floor(Math.random() * spawnPoints.length)]; 11 | 12 | room?.send("spawnSelf", { 13 | x: spawnPoint.x, 14 | y: spawnPoint.y, 15 | }); 16 | }); 17 | 18 | return null; 19 | } 20 | -------------------------------------------------------------------------------- /client/src/components/player/Players.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | PlayerHealthState, 3 | PlayerState, 4 | } from "../../../../server/src/rooms/schema/MyRoomState"; 5 | import { useColyseusRoom, useColyseusState } from "../../colyseus"; 6 | import { Container, Sprite, useTick } from "@pixi/react"; 7 | import { useLerped, useLerpedRadian } from "../../lib/useLerped"; 8 | import { PlayerSprite } from "./PlayerSprite"; 9 | import { PlayerSelf } from "./PlayerSelf"; 10 | import Matter, { Body } from "matter-js"; 11 | import { useBodyRef } from "../../lib/physics/hooks"; 12 | import { SpectateControls } from "./SpectateControls"; 13 | import { useState } from "react"; 14 | import { useRoomMessageHandler, useSelf } from "../../lib/networking/hooks"; 15 | import { playSelfDied } from "../../lib/sound/sound"; 16 | import { Texture } from "pixi.js"; 17 | import { getMaxHealth } from "../../../../server/src/game/player"; 18 | import { getEntityFilters } from "../graphics/filters"; 19 | 20 | export function Players() { 21 | const state = useColyseusState(); 22 | const players = state?.players; 23 | const self = useSelf(); 24 | 25 | useRoomMessageHandler("playerDied", (message) => { 26 | if (message.playerId === self.sessionId) { 27 | playSelfDied(); 28 | } 29 | }); 30 | 31 | if (!players) { 32 | return null; 33 | } 34 | 35 | return ( 36 | 37 | {Array.from(players.entries()).map(([id, player]) => ( 38 | 39 | ))} 40 | 41 | ); 42 | } 43 | 44 | function Player({ player }: { player: PlayerState }) { 45 | const sessionId = useColyseusRoom()?.sessionId; 46 | const isMe = player.sessionId === sessionId; 47 | 48 | if (isMe) { 49 | return player.healthState == PlayerHealthState.ALIVE ? ( 50 | 51 | ) : ( 52 | <> 53 | 54 | {PlayerHealthState.DEAD && } 55 | 56 | ); 57 | } else { 58 | return ; 59 | } 60 | } 61 | 62 | function OtherPlayer({ player }: { player: PlayerState }) { 63 | if (player.healthState === PlayerHealthState.ALIVE) { 64 | return ; 65 | } else if (player.healthState === PlayerHealthState.DEAD) { 66 | return ; 67 | } else { 68 | return null; 69 | } 70 | } 71 | 72 | function PlayerGrave({ x, y }: { x: number; y: number }) { 73 | const rotation = useState(Math.random() * Math.PI * 2)[0]; 74 | return ( 75 | 83 | ); 84 | } 85 | 86 | function OtherAlivePlayer({ player }: { player: PlayerState }) { 87 | const x = useLerped(player.x, 0.5); 88 | const y = useLerped(player.y, 0.5); 89 | const rotation = useLerpedRadian(player.rotation, 0.5); 90 | const collider = useBodyRef(() => { 91 | return Matter.Bodies.circle(player.x, player.y, 40); 92 | }); 93 | useTick(() => { 94 | Body.setPosition(collider.current, { 95 | x, 96 | y, 97 | }); 98 | }); 99 | 100 | return ( 101 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /client/src/components/player/SpectateControls.tsx: -------------------------------------------------------------------------------- 1 | import { useTick } from "@pixi/react"; 2 | import { useEffect, useState } from "react"; 3 | import { useCurrentPlayerDirection } from "../../lib/useControls"; 4 | import { useCameraStore } from "../graphics/cameraStore"; 5 | 6 | export function SpectateControls({ x, y }: { x: number; y: number }) { 7 | const [realX, setRealX] = useState(x); 8 | const [realY, setRealY] = useState(y); 9 | 10 | const direction = useCurrentPlayerDirection(); 11 | 12 | useTick((delta) => { 13 | setRealX((x) => x + direction.x * 3 * delta); 14 | setRealY((y) => y + direction.y * 3 * delta); 15 | }); 16 | 17 | x = realX; 18 | y = realY; 19 | 20 | const { setPosition, setZoom } = useCameraStore(); 21 | 22 | useEffect(() => { 23 | setPosition(x, y); 24 | setZoom(0.5); 25 | }, [x, y, setPosition]); 26 | 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/stageContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | interface CameraContext { 5 | camera: PIXI.Container | null; 6 | } 7 | 8 | export const cameraContext = createContext(null); 9 | export const CameraProvider = cameraContext.Provider; 10 | 11 | export function useCamera() { 12 | const context = useContext(cameraContext); 13 | if (!context) { 14 | throw new Error("useCamera must be used within a CameraProvider"); 15 | } 16 | return context?.camera; 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/testlevel.json: -------------------------------------------------------------------------------- 1 | { 2 | "objects": [ 3 | { 4 | "x": 1186, 5 | "y": -133, 6 | "id": "80elo", 7 | "scale": 1, 8 | "spawns": "player", 9 | "rotation": 0, 10 | "objectType": "spawnPoint" 11 | }, 12 | { 13 | "x": 1272, 14 | "y": 191, 15 | "id": "ta0p8", 16 | "scale": 1, 17 | "width": 1000, 18 | "height": 10, 19 | "sprite": { 20 | "assetUrl": "/assets/editor/missing.png", 21 | "assetSource": "external" 22 | }, 23 | "tiling": true, 24 | "rotation": 0, 25 | "colliders": [ 26 | { 27 | "x": 0, 28 | "y": 0, 29 | "shape": { 30 | "shape": "rectangle", 31 | "width": 1000, 32 | "height": 10 33 | }, 34 | "rotation": 0 35 | } 36 | ], 37 | "objectType": "asset" 38 | }, 39 | { 40 | "x": 1096, 41 | "y": 622, 42 | "id": "ytbnb", 43 | "scale": 1, 44 | "spawns": "zombie", 45 | "rotation": 0, 46 | "objectType": "spawnPoint" 47 | }, 48 | { 49 | "x": 995, 50 | "y": -678, 51 | "id": "1nn2d", 52 | "scale": 1, 53 | "width": 100, 54 | "height": 100, 55 | "sprite": { 56 | "assetUrl": "/assets/editor/missing.png", 57 | "assetSource": "external" 58 | }, 59 | "tiling": false, 60 | "rotation": 0, 61 | "colliders": [ 62 | { 63 | "x": 0, 64 | "y": 0, 65 | "shape": { 66 | "shape": "rectangle", 67 | "width": 150, 68 | "height": 150 69 | }, 70 | "rotation": 0 71 | } 72 | ], 73 | "objectType": "asset" 74 | }, 75 | { 76 | "x": 809, 77 | "y": -264, 78 | "id": "tbaiu", 79 | "scale": 1, 80 | "width": 10, 81 | "height": 1000, 82 | "sprite": { 83 | "assetUrl": "/assets/editor/missing.png", 84 | "assetSource": "external" 85 | }, 86 | "tiling": true, 87 | "rotation": 0, 88 | "colliders": [ 89 | { 90 | "x": 0, 91 | "y": 0, 92 | "shape": { 93 | "shape": "rectangle", 94 | "width": 10, 95 | "height": 1000 96 | }, 97 | "rotation": 0 98 | } 99 | ], 100 | "objectType": "asset" 101 | }, 102 | { 103 | "x": 234, 104 | "y": -45, 105 | "id": "ej9tm", 106 | "scale": 1, 107 | "spawns": "zombie", 108 | "rotation": 0, 109 | "objectType": "spawnPoint" 110 | } 111 | ] 112 | } -------------------------------------------------------------------------------- /client/src/components/ui/CharacterPreview.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Stage, TilingSprite, useTick } from "@pixi/react"; 2 | import React, { useState } from "react"; 3 | import { PlayerSprite } from "../player/PlayerSprite"; 4 | import { PlayerClass } from "../../../../server/src/game/player"; 5 | import { Texture } from "pixi.js"; 6 | const PREVIEW_SIZE = 250; 7 | 8 | export function CharacterPreview({ 9 | name, 10 | selectedClass, 11 | }: { 12 | name: string; 13 | selectedClass: PlayerClass; 14 | }) { 15 | return ( 16 | 23 | 29 | 30 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function Floor() { 47 | const [floorX, setFloorX] = useState(0); 48 | useTick((delta) => { 49 | setFloorX((x) => (x - 1.8 * delta) % (PREVIEW_SIZE * 99)); 50 | }); 51 | 52 | return ( 53 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /client/src/components/ui/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from "react"; 2 | import { useUIStore } from "./uiStore"; 3 | import { twMerge } from "tailwind-merge"; 4 | import { useColyseusRoom } from "../../colyseus"; 5 | import { useRoomMessageHandler } from "../../lib/networking/hooks"; 6 | import { useClientCommandInterceptor } from "../util/clientCommands"; 7 | 8 | export function Chat() { 9 | const [messages, setMessages] = useState< 10 | { 11 | content: string; 12 | time: Date; 13 | color?: string; 14 | }[] 15 | >([]); 16 | const { chatOpen, setChatOpen } = useUIStore(); 17 | const room = useColyseusRoom(); 18 | 19 | useRoomMessageHandler("chatMessage", (message) => { 20 | setMessages((messages) => [ 21 | ...messages, 22 | { 23 | content: message.message, 24 | time: new Date(), 25 | color: message.color, 26 | }, 27 | ]); 28 | }); 29 | 30 | const inputRef = useRef(null); 31 | 32 | const close = useCallback(() => { 33 | setChatOpen(false); 34 | inputRef.current?.blur(); 35 | inputRef.current && (inputRef.current.value = ""); 36 | }, [setChatOpen]); 37 | 38 | const interceptor = useClientCommandInterceptor((message) => { 39 | setMessages((messages) => [ 40 | ...messages, 41 | { 42 | content: message.message, 43 | time: new Date(), 44 | color: message.color, 45 | }, 46 | ]); 47 | }); 48 | 49 | const currentCallback = useRef(null); 50 | 51 | useEffect(() => { 52 | currentCallback.current = (e: KeyboardEvent) => { 53 | if (e.key === "Enter") { 54 | if (chatOpen) { 55 | if (inputRef.current?.value) { 56 | const message = interceptor(inputRef.current.value); 57 | if (message) room?.send("chatMessage", inputRef.current.value); 58 | } 59 | close(); 60 | } else { 61 | setChatOpen(true); 62 | inputRef.current?.focus(); 63 | } 64 | } else if (e.key === "Escape") { 65 | close(); 66 | } 67 | }; 68 | }, [chatOpen, setChatOpen, close, room, interceptor]); 69 | 70 | useEffect(() => { 71 | const callback = (e) => { 72 | currentCallback.current && currentCallback.current(e); 73 | }; 74 | window.addEventListener("keydown", callback); 75 | return () => { 76 | window.removeEventListener("keydown", callback); 77 | }; 78 | }, []); 79 | 80 | return ( 81 |
82 |
83 | {messages 84 | .filter((m) => { 85 | if (chatOpen) return true; 86 | return m.time.getTime() > Date.now() - 5000; 87 | }) 88 | .map((message, i) => ( 89 |
99 | {message.content} 100 |
101 | ))} 102 | 110 |
111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /client/src/components/ui/EscapeScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useUIStore } from "./uiStore"; 3 | import { disconnectFromColyseus } from "../../colyseus"; 4 | import { useClientSettings } from "./soundStore"; 5 | 6 | export function EscapeScreen() { 7 | const { volume, setVolume } = useClientSettings(); 8 | 9 | const { escapeOpen: open, setEscapeOpen: setOpen } = useUIStore(); 10 | 11 | const otherMenuOpen = useUIStore( 12 | (state) => state.buyMenuOpen || state.chatOpen 13 | ); 14 | 15 | useEffect(() => { 16 | const onKeyDown = (e: KeyboardEvent) => { 17 | if (otherMenuOpen) return; 18 | if (e.key === "Escape") { 19 | setOpen(!open); 20 | } 21 | }; 22 | window.addEventListener("keydown", onKeyDown); 23 | return () => { 24 | window.removeEventListener("keydown", onKeyDown); 25 | }; 26 | }, [open, otherMenuOpen, setOpen]); 27 | 28 | if (!open) { 29 | return null; 30 | } 31 | 32 | return ( 33 |
34 |
35 |
36 |

Escape Menu

37 |
{ 40 | setOpen(false); 41 | }} 42 | > 43 | X 44 |
45 |
46 |
47 | 53 | setVolume(parseFloat(e.target.value))} 61 | className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" 62 | /> 63 |
64 |
65 | 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /client/src/components/ui/LeaderBoard.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from "tailwind-merge"; 2 | import { calculateScore } from "../../../../server/src/game/player"; 3 | import { useColyseusState } from "../../colyseus"; 4 | import { useUIStore } from "./uiStore"; 5 | import { useIsKeyDown } from "../../lib/useControls"; 6 | import { useEffect } from "react"; 7 | 8 | export function LeaderBoard({ gameOver }: { gameOver: boolean }) { 9 | const { leaderboardOpen, setLeaderboardOpen } = useUIStore(); 10 | const state = useColyseusState(); 11 | const players = state?.players; 12 | const keyDown = useIsKeyDown("tab"); 13 | useEffect(() => { 14 | setLeaderboardOpen(keyDown || gameOver); 15 | }, [gameOver, keyDown, setLeaderboardOpen]); 16 | 17 | return ( 18 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {players && 39 | Array.from(players.values()) 40 | .map((player) => ({ 41 | ...player, 42 | score: calculateScore(player), 43 | })) 44 | .sort((a, b) => b.score - a.score) 45 | .map((player) => ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ))} 56 | 57 |
NameKillsDeathsAccuracyWaves SurvivedDamageScore
{player.name}{player.kills}{player.deaths}{player.accuracy}{player.wavesSurvived}{player.damageDealt}{player.score}
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /client/src/components/ui/characterCustomizationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { PlayerClass } from "../../../../server/src/game/player"; 3 | import { persist } from "zustand/middleware"; 4 | 5 | interface CharacterCustomizationStore { 6 | selectedClass: PlayerClass; 7 | setSelectedClass: (playerClass: PlayerClass) => void; 8 | name: string; 9 | setName: (name: string) => void; 10 | } 11 | 12 | export const useCharacterCustomizationStore = create( 13 | persist( 14 | (set) => ({ 15 | selectedClass: "pistol", 16 | setSelectedClass: (playerClass: PlayerClass) => 17 | set({ selectedClass: playerClass }), 18 | name: "", 19 | setName: (name: string) => set({ name }), 20 | }), 21 | { 22 | name: "characterCustomization", 23 | } 24 | ) 25 | ); 26 | -------------------------------------------------------------------------------- /client/src/components/ui/mainMenu/AuthSection.tsx: -------------------------------------------------------------------------------- 1 | import { useLogto } from "@logto/react"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export function AuthSection() { 5 | const { signIn, isAuthenticated } = useLogto(); 6 | 7 | if (isAuthenticated) return ; 8 | 9 | return ( 10 | 16 | ); 17 | } 18 | 19 | function UserInfo() { 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | const [userInfo, setUserInfo] = useState(null); 22 | const { fetchUserInfo, signOut } = useLogto(); 23 | useEffect(() => { 24 | fetchUserInfo().then((info) => { 25 | setUserInfo(info); 26 | }); 27 | }, [fetchUserInfo]); 28 | 29 | return ( 30 |
31 |
Logged in as {userInfo?.name}
32 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /client/src/components/ui/mainMenu/MapSelector.tsx: -------------------------------------------------------------------------------- 1 | import { MapInfo } from "../../../../../server/src/trpc/mapRouter"; 2 | import { trpc } from "../../../lib/trpc/trpcClient"; 3 | import { MapPreviewRenderer } from "../../level/LevelInstanceRenderer"; 4 | import { CenteredFullScreen } from "../uiUtils"; 5 | 6 | type onSelect = (mapId: string, mapName: string) => void; 7 | 8 | export function MapSelector({ 9 | open, 10 | onClose, 11 | onSelect, 12 | }: { 13 | open: boolean; 14 | onClose: () => void; 15 | onSelect: onSelect; 16 | }) { 17 | const maps = trpc.maps.getMapsToPlay.useQuery(); 18 | 19 | if (!open || !maps.data) return null; 20 | 21 | const _onSelect = (mapId: string, mapName: string) => { 22 | onSelect(mapId, mapName); 23 | onClose(); 24 | }; 25 | 26 | return ( 27 | 28 |
35 | 40 | {maps.data.myMaps && ( 41 | 46 | )} 47 | 52 |
53 |
54 | ); 55 | } 56 | 57 | function MapsSection({ 58 | title, 59 | maps, 60 | onSelect, 61 | }: { 62 | title: string; 63 | maps: MapInfo[]; 64 | onSelect: onSelect; 65 | }) { 66 | return ( 67 |
68 |

{title}

69 |
70 | {maps.map((map) => ( 71 | 72 | ))} 73 |
74 |
75 | ); 76 | } 77 | 78 | function MapCard({ info, onSelect }: { info: MapInfo; onSelect: onSelect }) { 79 | return ( 80 |
{ 83 | onSelect(info.id, info.name); 84 | }} 85 | > 86 | 87 |

88 | {info.name} 89 |

90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /client/src/components/ui/soundStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | interface ClientSettingsStore { 5 | volume: number; 6 | setVolume: (volume: number) => void; 7 | 8 | showFps: boolean; 9 | setShowFps: (showFps: boolean) => void; 10 | } 11 | 12 | export const useClientSettings = create( 13 | persist( 14 | (set) => ({ 15 | volume: 1, 16 | setVolume: (volume: number) => set({ volume }), 17 | 18 | showFps: false, 19 | setShowFps: (showFps: boolean) => set({ showFps }), 20 | }), 21 | { 22 | name: "clientSettings", 23 | } 24 | ) 25 | ); 26 | -------------------------------------------------------------------------------- /client/src/components/ui/uiStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface UIStore { 4 | buyMenuOpen: boolean; 5 | setBuyMenuOpen: (buyMenuOpen: boolean) => void; 6 | 7 | chatOpen: boolean; 8 | setChatOpen: (chatOpen: boolean) => void; 9 | 10 | leaderboardOpen: boolean; 11 | setLeaderboardOpen: (leaderboardOpen: boolean) => void; 12 | 13 | escapeOpen: boolean; 14 | setEscapeOpen: (escapeOpen: boolean) => void; 15 | } 16 | 17 | export const useUIStore = create((set) => ({ 18 | buyMenuOpen: false, 19 | setBuyMenuOpen: (buyMenuOpen: boolean) => set({ buyMenuOpen }), 20 | 21 | chatOpen: false, 22 | setChatOpen: (chatOpen: boolean) => set({ chatOpen }), 23 | 24 | leaderboardOpen: false, 25 | setLeaderboardOpen: (leaderboardOpen: boolean) => set({ leaderboardOpen }), 26 | 27 | escapeOpen: false, 28 | setEscapeOpen: (escapeOpen: boolean) => set({ escapeOpen }), 29 | })); 30 | -------------------------------------------------------------------------------- /client/src/components/ui/uiUtils.tsx: -------------------------------------------------------------------------------- 1 | export function CenteredFullScreen({ 2 | children, 3 | onClose, 4 | }: { 5 | children: React.ReactNode; 6 | onClose?: () => void; 7 | }) { 8 | return ( 9 |
{ 12 | onClose?.(); 13 | }} 14 | > 15 |
{ 17 | e.stopPropagation(); 18 | }} 19 | > 20 | {children} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/util/FpsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Text, useApp, useTick } from "@pixi/react"; 2 | import { useRef, useState } from "react"; 3 | 4 | export function FpsTracker() { 5 | const app = useApp(); 6 | const [fps, setFps] = useState(0); 7 | 8 | const rollingAverage = useRef([]); 9 | 10 | useTick(() => { 11 | rollingAverage.current.push(app.ticker.FPS); 12 | if (rollingAverage.current.length > 200) { 13 | rollingAverage.current.shift(); 14 | } 15 | 16 | setFps( 17 | rollingAverage.current.reduce((acc, val) => acc + val, 0) / 18 | rollingAverage.current.length 19 | ); 20 | }); 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/util/Spinner.tsx: -------------------------------------------------------------------------------- 1 | export function Spinner() { 2 | return ( 3 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/util/clientCommands.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { disconnectFromColyseus, useColyseusState } from "../../colyseus"; 3 | import { trpc } from "../../lib/trpc/trpcClient"; 4 | import { useClientSettings } from "../ui/soundStore"; 5 | 6 | export function useClientCommandInterceptor( 7 | respond: ({ message }: { message: string; color?: string }) => void 8 | ) { 9 | const testConnection = trpc.testConnection.useMutation(); 10 | const verifyMap = trpc.maps.verifyMap.useMutation(); 11 | const currentMapId = useColyseusState((state) => state.mapId); 12 | 13 | return useCallback( 14 | (message: string) => { 15 | if (!message.startsWith("/")) return message; 16 | const command = message.substring(1).split(" ")[0].toLowerCase(); 17 | switch (command) { 18 | case "disconnect": 19 | case "leave": 20 | disconnectFromColyseus(); 21 | break; 22 | case "test": 23 | testConnection.mutateAsync().then((msg) => respond({ message: msg })); 24 | break; 25 | case "showfps": 26 | useClientSettings.getState().setShowFps(true); 27 | break; 28 | case "verifymap": 29 | if (!currentMapId) { 30 | return respond({ 31 | message: "You must be in a map to verify it", 32 | color: "red", 33 | }); 34 | } 35 | verifyMap 36 | .mutateAsync({ 37 | mapId: currentMapId, 38 | verify: true, 39 | }) 40 | .then((msg) => respond({ message: msg })); 41 | break; 42 | default: 43 | return message; 44 | } 45 | }, 46 | [respond, testConnection, currentMapId, verifyMap] 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/zombies/MyZombie.tsx: -------------------------------------------------------------------------------- 1 | import { ZombieState } from "../../../../server/src/rooms/schema/MyRoomState"; 2 | import { ZombieSprite } from "./Zombies"; 3 | import { useZombieBulletHitListener } from "./zombieHooks"; 4 | 5 | import { useZombieLogic } from "./zombieLogic"; 6 | import { useLerpedRadian } from "../../lib/useLerped"; 7 | import { useZombieColliders } from "./zombieColliders"; 8 | 9 | export function MyZombie({ zombie }: { zombie: ZombieState }) { 10 | const colliders = useZombieColliders( 11 | zombie.zombieType, 12 | zombie.id, 13 | zombie.x, 14 | zombie.y 15 | ); 16 | 17 | useZombieBulletHitListener(colliders.hitBox.current, zombie.id); 18 | 19 | const { x, y, rotation } = useZombieLogic(zombie, colliders.collider); 20 | 21 | const visibleRotation = useLerpedRadian(rotation, 0.03); 22 | 23 | return ( 24 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/zombies/ZombieSpawner.tsx: -------------------------------------------------------------------------------- 1 | import { useRoomMessageHandler } from "../../lib/networking/hooks"; 2 | import { useColyseusRoom, useColyseusState } from "../../colyseus"; 3 | import { PlayerState } from "../../../../server/src/rooms/schema/MyRoomState"; 4 | import { useSpawnPoints } from "../level/spawnPointContext"; 5 | 6 | export function ZombieSpawner() { 7 | const spawnPoints = useSpawnPoints("zombie"); 8 | 9 | const players = useColyseusState((state) => state.players); 10 | const room = useColyseusRoom(); 11 | useRoomMessageHandler("requestSpawnZombie", ({ type, respawnId }) => { 12 | const playersList = Array.from(players!.values()); 13 | 14 | // calculate the distance to the closest player for all spawn points 15 | const spawnPointDistances = spawnPoints 16 | .map(({ x, y }) => { 17 | const closestPlayer = playersList.reduce( 18 | (closest, player) => { 19 | const distance = Math.hypot(player.x - x, player.y - y); 20 | if (distance < closest.distance) { 21 | return { player, distance }; 22 | } 23 | return closest; 24 | }, 25 | { player: undefined as undefined | PlayerState, distance: Infinity } 26 | ); 27 | 28 | return { x, y, distance: closestPlayer.distance }; 29 | }) 30 | .sort((a, b) => a.distance - b.distance); 31 | 32 | const MIN_DISTANCE = 1500; 33 | const spawnPoint = 34 | spawnPointDistances.find((point) => point.distance > MIN_DISTANCE) ?? 35 | spawnPointDistances[0]; 36 | 37 | room?.send("spawnZombie", { 38 | x: spawnPoint.x, 39 | y: spawnPoint.y, 40 | type, 41 | respawnId, 42 | }); 43 | }); 44 | 45 | return null; 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/zombies/zombieColliders.ts: -------------------------------------------------------------------------------- 1 | import { zombieInfo } from "./../../../../server/src/game/zombies"; 2 | import Matter from "matter-js"; 3 | import { ZombieType } from "../../../../server/src/game/zombies"; 4 | import { useBodyRef } from "../../lib/physics/hooks"; 5 | import { useTick } from "@pixi/react"; 6 | 7 | export function useZombieColliders( 8 | zombieType: ZombieType, 9 | zombieId: number, 10 | initialX: number, 11 | initialY: number 12 | ) { 13 | const info = zombieInfo[zombieType]; 14 | const hitBox = useBodyRef( 15 | () => { 16 | return Matter.Bodies.circle(initialX, initialY, 40 * info.size, { 17 | density: 0.003, 18 | isSensor: true, 19 | }); 20 | }, 21 | { tags: ["zombie", "zombieHitBox"], id: zombieId } 22 | ); 23 | const collider = useBodyRef( 24 | () => { 25 | return Matter.Bodies.circle(initialX, initialY, 30 * info.size, { 26 | density: 0.003, 27 | }); 28 | }, 29 | { tags: ["zombie", "zombieCollider"], id: zombieId } 30 | ); 31 | 32 | useTick(() => { 33 | // move the hitBox to the collider 34 | Matter.Body.setPosition(hitBox.current, collider.current.position); 35 | }); 36 | 37 | return { hitBox, collider }; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/zombies/zombieHooks.ts: -------------------------------------------------------------------------------- 1 | import { Body } from "matter-js"; 2 | import { useBulletHitListener } from "../bullets/bullet"; 3 | import { useColyseusRoom } from "../../colyseus"; 4 | import { useEffect } from "react"; 5 | import { playZombieGrowl } from "../../lib/sound/sound"; 6 | 7 | export function useZombieBulletHitListener(body: Body, zombieId: number) { 8 | const room = useColyseusRoom(); 9 | useBulletHitListener(body, (bullet) => { 10 | room?.send("zombieHit", { zombieId, bulletId: bullet.id }); 11 | }); 12 | } 13 | 14 | const nextGrowl = () => Math.random() * 5000 + 2000; 15 | export function useGrowling() { 16 | useEffect(() => { 17 | const callback = () => { 18 | playZombieGrowl(); 19 | timeout = setTimeout(callback, nextGrowl()); 20 | }; 21 | let timeout = setTimeout(callback, 100); 22 | return () => { 23 | clearTimeout(timeout); 24 | }; 25 | }, []); 26 | } 27 | -------------------------------------------------------------------------------- /client/src/editor/CreateAssetsUI.tsx: -------------------------------------------------------------------------------- 1 | import { MapObject } from "../../../server/src/game/mapEditor/editorTypes"; 2 | import { useEditor } from "./mapEditorStore"; 3 | 4 | export function CreateAssetsMenu() { 5 | const addObject = useEditor((state) => state.addObject); 6 | const setSelectedObject = useEditor((state) => state.setSelectedObject); 7 | 8 | const x = useEditor((state) => state.cameraX); 9 | const y = useEditor((state) => state.cameraY); 10 | 11 | const create = (obj: MapObject) => { 12 | addObject(obj); 13 | setSelectedObject(obj.id); 14 | }; 15 | 16 | return ( 17 |
18 |
19 | Create 20 | 26 | 34 | 35 |
36 |
    40 |
  • 41 | 65 |
  • 66 |
  • 67 | 83 |
  • 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /client/src/editor/EditorCamera.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from "./mapEditorStore"; 2 | import { useApp, useTick } from "@pixi/react"; 3 | import { ReactNode, useEffect } from "react"; 4 | import { 5 | useControlEventListeners, 6 | useCurrentPlayerDirection, 7 | } from "../lib/useControls"; 8 | import { GenericCamera } from "../components/graphics/Camera"; 9 | 10 | export function EditorControls() { 11 | useControlEventListeners(false); 12 | const zoom = useEditor((state) => state.zoom); 13 | const setCamera = useEditor((state) => state.setCamera); 14 | const cameraX = useEditor((state) => state.cameraX); 15 | const cameraY = useEditor((state) => state.cameraY); 16 | // const app = useApp(); 17 | 18 | useEffect(() => { 19 | const wheelListener = (e: WheelEvent) => { 20 | useEditor 21 | .getState() 22 | .setZoom(minmax(useEditor.getState().zoom + e.deltaY * 0.001, 0.1, 10)); 23 | }; 24 | window.addEventListener("wheel", wheelListener); 25 | return () => { 26 | window.removeEventListener("wheel", wheelListener); 27 | }; 28 | }, []); 29 | 30 | const dir = useCurrentPlayerDirection(10); 31 | 32 | const SPEED = 5 / zoom / 2; 33 | 34 | useTick((delta) => { 35 | setCamera(cameraX + dir.x * delta * SPEED, cameraY + dir.y * delta * SPEED); 36 | }); 37 | 38 | return null; 39 | } 40 | 41 | export function EditorCamera({ children }: { children: ReactNode }) { 42 | const x = useEditor((state) => state.cameraX); 43 | const y = useEditor((state) => state.cameraY); 44 | const zoom = useEditor((state) => state.zoom); 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ); 51 | } 52 | 53 | function minmax(value: number, min: number, max: number) { 54 | return Math.max(min, Math.min(max, value)); 55 | } 56 | -------------------------------------------------------------------------------- /client/src/editor/MapEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Sprite } from "@pixi/react"; 2 | import { SpawnPoint } from "../../../server/src/game/mapEditor/editorTypes"; 3 | import { FullScreenStage } from "../components/graphics/FullScreenStage"; 4 | import { EditorCamera, EditorControls } from "./EditorCamera"; 5 | import { useEditor } from "./mapEditorStore"; 6 | import { Texture } from "pixi.js"; 7 | import { ComponentProps, useEffect, useMemo } from "react"; 8 | import { MapEditorUI } from "./MapEditorUI"; 9 | import { spriteSheets } from "../assets/assetHandler"; 10 | import { TempFloor } from "../components/level/LevelInstanceRenderer"; 11 | import { LevelObject } from "./LevelObject"; 12 | import { useEntityShadow } from "../components/graphics/filters"; 13 | 14 | export function MapEditor() { 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ); 28 | } 29 | 30 | function VisualObjectsEditor() { 31 | const objects = useEditor((state) => state.level.objects); 32 | useEffect(() => { 33 | const keydown = (e: KeyboardEvent) => { 34 | if (e.key === "Escape") { 35 | useEditor.getState().setSelectedObject(null); 36 | } else if (e.key == "Delete") { 37 | if (useEditor.getState().selectedObject) { 38 | useEditor 39 | .getState() 40 | .deleteObject(useEditor.getState().selectedObject); 41 | } 42 | } 43 | }; 44 | window.addEventListener("keydown", keydown); 45 | return () => { 46 | window.removeEventListener("keydown", keydown); 47 | }; 48 | }, []); 49 | 50 | const returning = ( 51 | 52 | {objects.map((object) => { 53 | return ; 54 | })} 55 | 56 | ); 57 | return returning; 58 | } 59 | 60 | export function SpawnPointDisplay({ 61 | spawnPoint, 62 | ...props 63 | }: { spawnPoint: SpawnPoint } & Partial>) { 64 | const scale = spawnPoint.spawns == "zombie" ? 0.4 : 0.5; 65 | 66 | const playerTexture = useMemo( 67 | () => 68 | Texture.from("Top_Down_Survivor/rifle/idle/survivor-idle_rifle_0.png"), 69 | [] 70 | ); 71 | 72 | const shadow = useEntityShadow(); 73 | 74 | return ( 75 | 76 | ({ 86 | x: 0.5, 87 | y: 0.5, 88 | }), 89 | [] 90 | )} 91 | {...props} 92 | /> 93 | 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /client/src/editor/VisualColliders.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Graphics } from "@pixi/react"; 2 | import { 3 | AssetCollider, 4 | AssetObject, 5 | } from "../../../server/src/game/mapEditor/editorTypes"; 6 | import { memo, useCallback } from "react"; 7 | import * as PIXI from "pixi.js"; 8 | import lodash from "lodash"; 9 | 10 | export function VisualColliders({ asset }: { asset: AssetObject }) { 11 | return ( 12 | 18 | {asset.colliders.map((collider, i) => { 19 | return ; 20 | })} 21 | 22 | ); 23 | } 24 | function _VisualCollider({ collider }: { collider: AssetCollider }) { 25 | const draw = useCallback( 26 | (g: PIXI.Graphics) => { 27 | g.clear(); 28 | 29 | // only outline for now 30 | g.lineStyle(4, collider.destroyBullet ? 0x00ff00 : 0x0000ff); 31 | if (collider.shape.shape === "circle") { 32 | g.drawCircle(0, 0, collider.shape.radius); 33 | } else if (collider.shape.shape === "rectangle") { 34 | g.drawRect( 35 | -collider.shape.width / 2, 36 | -collider.shape.height / 2, 37 | collider.shape.width, 38 | collider.shape.height 39 | ); 40 | } 41 | }, 42 | [ 43 | collider.destroyBullet, 44 | collider.shape.shape, 45 | // @ts-expect-error - is not casted 46 | collider.shape.radius, 47 | // @ts-expect-error - is not casted 48 | collider.shape.width, 49 | // @ts-expect-error - is not casted 50 | collider.shape.height, 51 | ] 52 | ); 53 | 54 | return ( 55 | 61 | ); 62 | } 63 | const VisualCollider = memo(_VisualCollider, (prev, next) => { 64 | return lodash.isEqual(prev.collider, next.collider); 65 | }); 66 | -------------------------------------------------------------------------------- /client/src/editor/assets/hooks.ts: -------------------------------------------------------------------------------- 1 | import { trpc } from "../../lib/trpc/trpcClient"; 2 | 3 | export function useCustomAssetBaseUrl() { 4 | return trpc.maps.assets.assetsEndpoint.useQuery().data; 5 | } 6 | 7 | export function useCustomAsset(uploadId: string) { 8 | const baseUrl = useCustomAssetBaseUrl(); 9 | return `${baseUrl}/asset/${uploadId}.png`; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | } 6 | 7 | .button { 8 | @apply btn; 9 | } 10 | 11 | .ui-text { 12 | text-shadow: 3px 5px 2px #474747; 13 | user-select: none; 14 | color: white; 15 | font-weight: bold; 16 | } 17 | 18 | @tailwind base; 19 | @tailwind components; 20 | @tailwind utilities; 21 | -------------------------------------------------------------------------------- /client/src/lib/auth/colyseusAuth.ts: -------------------------------------------------------------------------------- 1 | import { useLogto } from "@logto/react"; 2 | import { useEffect } from "react"; 3 | import { colyseusClient } from "../../colyseus"; 4 | 5 | export function useSetColyseusAuthToken() { 6 | const { isAuthenticated, getAccessToken } = useLogto(); 7 | 8 | useEffect(() => { 9 | (async () => { 10 | if (isAuthenticated) { 11 | const accessToken = await getAccessToken( 12 | "https://apocalypse.p3ntest.dev/" 13 | ); 14 | colyseusClient.auth.token = accessToken; 15 | } else { 16 | colyseusClient.auth.token = undefined; 17 | } 18 | })(); 19 | }, [isAuthenticated, getAccessToken]); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/lib/auth/logto.ts: -------------------------------------------------------------------------------- 1 | import { LogtoConfig } from "@logto/react"; 2 | 3 | export const logtoConfig: LogtoConfig = { 4 | endpoint: "https://zombies-auth.p3ntest.dev/", 5 | appId: "k4uywxphxu338dkjyqeqs", 6 | scopes: ["verify:maps"], 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/lib/gameStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface GameStore { 4 | mouseX: number; 5 | mouseY: number; 6 | setMousePosition: (x: number, y: number) => void; 7 | } 8 | 9 | export const useGameStore = create((set) => ({ 10 | mouseX: 0, 11 | mouseY: 0, 12 | setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }), 13 | })); 14 | -------------------------------------------------------------------------------- /client/src/lib/graphics/useAsset.ts: -------------------------------------------------------------------------------- 1 | import { Assets, Texture } from "pixi.js"; 2 | import { useEffect, useState } from "react"; 3 | 4 | const assetMap = new Map(); 5 | const loadCallbacks = new Map void>>(); 6 | 7 | export function useAsset(url: string): Texture | null { 8 | const [asset, setAsset] = useState(null); 9 | 10 | useEffect(() => { 11 | if (assetMap.has(url)) { 12 | const asset = assetMap.get(url); 13 | if (asset === "loading") { 14 | if (!loadCallbacks.has(url)) { 15 | loadCallbacks.set(url, new Set()); 16 | } 17 | loadCallbacks.get(url)!.add(() => { 18 | setAsset(assetMap.get(url)! as Texture); 19 | }); 20 | } else { 21 | setAsset(asset as Texture); 22 | } 23 | } else { 24 | assetMap.set(url, "loading"); 25 | Assets.load(url).then((texture) => { 26 | assetMap.set(url, texture); 27 | if (loadCallbacks.has(url)) { 28 | for (const callback of loadCallbacks.get(url)!) { 29 | callback(); 30 | } 31 | loadCallbacks.delete(url); 32 | } 33 | }); 34 | } 35 | }, [url]); 36 | 37 | return asset; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/lib/hooks/usePlayers.ts: -------------------------------------------------------------------------------- 1 | import { useColyseusState } from "../../colyseus"; 2 | import { PlayerHealthState } from "../../../../server/src/rooms/schema/MyRoomState"; 3 | 4 | export function usePlayers() { 5 | const playerMap = useColyseusState((state) => state.players); 6 | return playerMap ? Array.from(playerMap.values()) : []; 7 | } 8 | 9 | export function useAlivePlayers() { 10 | const players = usePlayers(); 11 | return players.filter( 12 | (player) => player.healthState == PlayerHealthState.ALIVE 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/src/lib/networking/batches.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export const zombieUpdatesBatch: Set = new Set(); 3 | -------------------------------------------------------------------------------- /client/src/lib/networking/rooms.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { MyRoomState } from "../../../../server/src/rooms/schema/MyRoomState"; 3 | import { colyseusClient, setCurrentRoom } from "../../colyseus"; 4 | import { useCharacterCustomizationStore } from "../../components/ui/characterCustomizationStore"; 5 | 6 | let connecting = false; 7 | 8 | export function useRoomMethods() { 9 | const playerOptions = useCharacterCustomizationStore(); 10 | const roomOptsBase = { 11 | playerClass: playerOptions.selectedClass, 12 | }; 13 | 14 | return { 15 | async createRoom(opts: any) { 16 | if (connecting) { 17 | return; 18 | } 19 | connecting = true; 20 | const room = await colyseusClient.create("my_room", { 21 | ...roomOptsBase, 22 | ...opts, 23 | }); 24 | setCurrentRoom(room); 25 | }, 26 | async quickPlay() { 27 | if (connecting) { 28 | return; 29 | } 30 | connecting = true; 31 | const room = await colyseusClient.joinOrCreate("my_room", { 32 | ...roomOptsBase, 33 | }); 34 | setCurrentRoom(room); 35 | }, 36 | async singlePlayer() { 37 | if (connecting) { 38 | return; 39 | } 40 | connecting = true; 41 | const room = await colyseusClient.create("my_room", { 42 | ...roomOptsBase, 43 | singlePlayer: true, 44 | }); 45 | setCurrentRoom(room); 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /client/src/lib/physics/PhysicsProvider.tsx: -------------------------------------------------------------------------------- 1 | import Matter from "matter-js"; 2 | import { useRef, useEffect } from "react"; 3 | import { PhysicsContextProvider } from "./context"; 4 | import { PhysicsTicker } from "./ticker"; 5 | 6 | export function PhysicsProvider({ children }: { children: React.ReactNode }) { 7 | const engine = useRef( 8 | Matter.Engine.create({ 9 | gravity: { 10 | x: 0, 11 | y: 0, 12 | }, 13 | }) 14 | ); 15 | 16 | const ticker = useRef( 17 | new PhysicsTicker((delta: number) => { 18 | Matter.Engine.update(engine.current, delta); 19 | }) 20 | ); 21 | 22 | useEffect(() => { 23 | const currentTicker = ticker.current; 24 | currentTicker.start(); 25 | return () => { 26 | currentTicker.stop(); 27 | }; 28 | }, []); 29 | 30 | return ( 31 | 37 | {children} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/lib/physics/context.ts: -------------------------------------------------------------------------------- 1 | import type Matter from "matter-js"; 2 | import { createContext } from "react"; 3 | import { PhysicsTicker } from "./ticker"; 4 | 5 | interface PhysicsContextState { 6 | engine: Matter.Engine; 7 | ticker: PhysicsTicker; 8 | } 9 | 10 | export const physicsContext = createContext(null); 11 | export const PhysicsContextProvider = physicsContext.Provider; 12 | -------------------------------------------------------------------------------- /client/src/lib/physics/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef } from "react"; 2 | import { physicsContext } from "./context"; 3 | import Matter, { Composite } from "matter-js"; 4 | 5 | export function usePhysicsWorld() { 6 | const context = useContext(physicsContext); 7 | if (!context) { 8 | throw new Error("usePhysicsWorld must be used within a PhysicsProvider"); 9 | } 10 | return context.engine.world; 11 | } 12 | 13 | export function usePhysicsEngine() { 14 | const context = useContext(physicsContext); 15 | if (!context) { 16 | throw new Error("usePhysicsEngine must be used within a PhysicsProvider"); 17 | } 18 | return context.engine; 19 | } 20 | 21 | type BodyMeta = { 22 | tags?: string[]; 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | [key: string]: any; 25 | }; 26 | 27 | export const bodyMeta = new Map(); 28 | 29 | export function getBodiesWithTag(tag: string) { 30 | const result: Matter.Body[] = []; 31 | for (const [body, meta] of bodyMeta) { 32 | if (meta.tags?.includes(tag)) { 33 | result.push(body); 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | export function getMetaForBody(body: Matter.Body) { 40 | return bodyMeta.get(body); 41 | } 42 | 43 | export function getBodyMeta(body: Matter.Body) { 44 | return bodyMeta.get(body); 45 | } 46 | 47 | export function useBodyRef(factory: () => Matter.Body, meta?: BodyMeta) { 48 | const world = usePhysicsWorld(); 49 | const body = useRef(null!); 50 | if (!body.current) { 51 | body.current = factory(); 52 | } 53 | 54 | useEffect(() => { 55 | if (!world) return; 56 | 57 | const current = body.current!; 58 | Composite.add(world, current); 59 | return () => { 60 | Composite.remove(world, current); 61 | }; 62 | }, [world, body]); 63 | 64 | useEffect(() => { 65 | if (meta) { 66 | bodyMeta.set(body.current, meta); 67 | } 68 | return () => { 69 | bodyMeta.delete(body.current); 70 | }; 71 | }, [meta, body]); 72 | 73 | return body!; 74 | } 75 | 76 | export function useOnCollisionStart( 77 | callback: (pairs: Matter.IEventCollision["pairs"][0]) => void 78 | ) { 79 | const engine = usePhysicsEngine(); 80 | useEffect(() => { 81 | const handler = (event: Matter.IEvent) => { 82 | const collisionEvent = event as Matter.IEventCollision; 83 | if (!collisionEvent.pairs) return; 84 | for (const pair of collisionEvent.pairs) { 85 | callback(pair); 86 | } 87 | }; 88 | Matter.Events.on(engine, "collisionStart", handler); 89 | return () => { 90 | Matter.Events.off(engine, "collisionStart", handler); 91 | }; 92 | }, [engine, callback]); 93 | } 94 | 95 | export function useFilteredOnCollisionStart( 96 | filter: Matter.Body, 97 | callback: ( 98 | pair: Matter.IEventCollision["pairs"][0] & { 99 | bodySelf: Matter.Body; 100 | bodyOther: Matter.Body; 101 | } 102 | ) => void 103 | ) { 104 | useOnCollisionStart((pair) => { 105 | const { bodyA, bodyB } = pair; 106 | if (bodyA === filter || bodyB === filter) { 107 | callback({ 108 | ...pair, 109 | bodySelf: bodyA === filter ? bodyA : bodyB, 110 | bodyOther: bodyA === filter ? bodyB : bodyA, 111 | }); 112 | } 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /client/src/lib/physics/ticker.tsx: -------------------------------------------------------------------------------- 1 | export class PhysicsTicker { 2 | running: boolean = false; 3 | tick: number = 0; 4 | lastTick: number = 0; 5 | 6 | constructor(private physicsUpdate: (delta: number) => void) {} 7 | 8 | start() { 9 | this.running = true; 10 | this.tick = 0; 11 | this.lastTick = Date.now(); 12 | this.loop(); 13 | } 14 | 15 | stop() { 16 | this.running = false; 17 | } 18 | 19 | loop() { 20 | if (!this.running) { 21 | return; 22 | } 23 | 24 | const now = Date.now(); 25 | const delta = now - this.lastTick; 26 | this.lastTick = now; 27 | 28 | this.tick += delta; 29 | 30 | const maxDelta = 300; 31 | 32 | this.update(Math.min(delta, maxDelta)); 33 | 34 | requestAnimationFrame(() => this.loop()); 35 | } 36 | 37 | beforeHandlers = new Set<() => void>(); 38 | afterHandlers = new Set<() => void>(); 39 | 40 | addBeforeHandler(handler: () => void) { 41 | this.beforeHandlers.add(handler); 42 | } 43 | 44 | removeBeforeHandler(handler: () => void) { 45 | this.beforeHandlers.delete(handler); 46 | } 47 | 48 | addAfterHandler(handler: () => void) { 49 | this.afterHandlers.add(handler); 50 | } 51 | 52 | removeAfterHandler(handler: () => void) { 53 | this.afterHandlers.delete(handler); 54 | } 55 | 56 | update(delta: number) { 57 | this.beforeHandlers.forEach((handler) => handler()); 58 | this.physicsUpdate(delta); 59 | this.afterHandlers.forEach((handler) => handler()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/src/lib/sound/sound.ts: -------------------------------------------------------------------------------- 1 | import { Assets } from "pixi.js"; 2 | import { PlayerClass } from "../../../../server/src/game/player"; 3 | import { useClientSettings } from "../../components/ui/soundStore"; 4 | import { Sound } from "@pixi/sound"; 5 | 6 | export function playSound( 7 | path: string, 8 | opts?: Partial<{ 9 | volume: number; 10 | }> 11 | ) { 12 | const { volume } = useClientSettings.getState(); 13 | Assets.load(path).then((s: Sound | null) => { 14 | if (!s) throw new Error("Sound not found " + path); 15 | 16 | s.volume = (opts?.volume ?? 1) * volume * 0.2; 17 | s.play(); 18 | }); 19 | // const audio = new Audio(path); 20 | // audio.volume = 0.2 * (opts?.volume ?? 1) * volume; 21 | 22 | // audio.play(); 23 | } 24 | 25 | export function playGunSound(gun: PlayerClass, msCoolDown: number = 1000) { 26 | switch (gun) { 27 | case "shotgun": 28 | playSound("/assets/sounds/guns/shotgun.mp3"); 29 | setTimeout(() => { 30 | playSound("/assets/sounds/guns/shotgun_rack.mp3"); 31 | }, Math.min(msCoolDown - 800, 500)); // 800ms is the sound length 32 | break; 33 | case "rifle": 34 | playSound("/assets/sounds/guns/rifle.mp3"); 35 | break; 36 | case "pistol": 37 | default: 38 | playSound("/assets/sounds/guns/9mm.mp3"); 39 | break; 40 | } 41 | } 42 | 43 | export function playZombieHitSound() { 44 | playSound("/assets/sounds/impact.ogg", { 45 | volume: 0.5, 46 | }); 47 | } 48 | 49 | export function playZombieDead() { 50 | playSound("/assets/sounds/zombieDeath.wav"); 51 | } 52 | 53 | export function playWaveStart() { 54 | playSound("/assets/sounds/waveStart.wav"); 55 | } 56 | 57 | const growls = new Array(16) 58 | .fill(0) 59 | .map((_, i) => `/assets/sounds/growls/monster/monster.${i + 1}.ogg`); 60 | 61 | export function playZombieGrowl(volume: number = 1) { 62 | playSound(growls[Math.floor(Math.random() * growls.length)], { 63 | volume: 0.5 * volume, 64 | }); 65 | } 66 | 67 | export function playSplat() { 68 | playSound("/assets/sounds/splat.ogg"); 69 | } 70 | 71 | export function playSelfDied() { 72 | playSound("/assets/sounds/playerdies.mp3", { 73 | volume: 2, 74 | }); 75 | } 76 | 77 | export function playHurtSound() { 78 | playSound("/assets/sounds/hurt.ogg"); 79 | } 80 | 81 | export function playMeleeSound(hit: boolean) { 82 | if (hit) { 83 | playSound("/assets/sounds/punch.mp3"); 84 | } else { 85 | playSound("/assets/sounds/punchMiss.mp3"); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /client/src/lib/trpc/TrpcWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useLogto } from "@logto/react"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | import { httpBatchLink } from "@trpc/client"; 4 | import { ReactNode, useEffect, useRef, useState } from "react"; 5 | import { trpc } from "./trpcClient"; 6 | import { backendUrl } from "./backendUrl"; 7 | 8 | export function TrpcWrapper({ children }: { children: ReactNode }) { 9 | const { getAccessToken, isAuthenticated } = useLogto(); 10 | 11 | const [queryClient] = useState(() => new QueryClient()); 12 | 13 | const getHeadersFunctionRef = useRef(async () => { 14 | if (!isAuthenticated) return {}; 15 | return { 16 | authorization: 17 | "Bearer " + (await getAccessToken("https://apocalypse.p3ntest.dev/")), 18 | }; 19 | }); 20 | 21 | useEffect(() => { 22 | getHeadersFunctionRef.current = async () => { 23 | if (!isAuthenticated) return {}; 24 | return { 25 | authorization: 26 | "Bearer " + (await getAccessToken("https://apocalypse.p3ntest.dev/")), 27 | }; 28 | }; 29 | }, [isAuthenticated, getAccessToken]); 30 | 31 | const [trpcClient] = useState(() => 32 | trpc.createClient({ 33 | links: [ 34 | httpBatchLink({ 35 | url: `${backendUrl}/trpc`, 36 | async headers() { 37 | return await getHeadersFunctionRef.current(); 38 | }, 39 | }), 40 | ], 41 | }) 42 | ); 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /client/src/lib/trpc/backendUrl.tsx: -------------------------------------------------------------------------------- 1 | export const backendUrl = 2 | window.location.hostname === "localhost" ? "http://localhost:2567" : ""; 3 | -------------------------------------------------------------------------------- /client/src/lib/trpc/trpcClient.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | import type { AppRouter } from "../../../../server/src/trpc/router"; 3 | 4 | export const trpc = createTRPCReact(); 5 | -------------------------------------------------------------------------------- /client/src/lib/useLerped.ts: -------------------------------------------------------------------------------- 1 | import { useTick } from "@pixi/react"; 2 | import { useState } from "react"; 3 | 4 | export function useLerped(value: number, factor: number) { 5 | const [lerped, setLerped] = useState(value); 6 | 7 | useTick((delta) => { 8 | setLerped((lerped) => lerped + (value - lerped) * factor * delta); 9 | }); 10 | 11 | return lerped; 12 | } 13 | 14 | export function useLerpedRadian(value: number, factor: number) { 15 | const [lerped, setLerped] = useState(value); 16 | 17 | useTick((deltaTime) => { 18 | setLerped((lerped) => { 19 | const diff = value - lerped; 20 | const delta = 21 | Math.atan2(Math.sin(diff), Math.cos(diff)) * factor * deltaTime; 22 | return lerped + delta; 23 | }); 24 | }); 25 | 26 | return lerped; 27 | } 28 | -------------------------------------------------------------------------------- /client/src/lib/useRerender.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export function useRerender() { 4 | const [, setTick] = useState(0); 5 | return () => setTick((tick) => tick + 1); 6 | } 7 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Router } from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /client/src/routes/callback.tsx: -------------------------------------------------------------------------------- 1 | import { useHandleSignInCallback } from "@logto/react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Spinner } from "../components/util/Spinner"; 4 | 5 | export function CallBackHandler() { 6 | const navigate = useNavigate(); 7 | const { isLoading } = useHandleSignInCallback(() => { 8 | console.log("Sign in callback"); 9 | navigate("/"); 10 | }); 11 | if (isLoading) return ; 12 | return
Done.
; 13 | } 14 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import daisy from "daisyui"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [daisy], 10 | daisyui: { 11 | themes: ["halloween"], 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "experimentalDecorators": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "strictNullChecks": false, 21 | "noImplicitAny": false 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /credits.md: -------------------------------------------------------------------------------- 1 | # Zombie Icons 2 | 3 | https://opengameart.org/content/zombie-ui-pack 4 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | bunx concurrently "cd client && bun dev" "cd server && bun start" -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /server/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: ["eslint:recommended"], 5 | ignorePatterns: ["dist", ".eslintrc.cjs"], 6 | parser: "@typescript-eslint/parser", 7 | plugins: ["unused-imports"], 8 | rules: { 9 | "unused-imports/no-unused-imports": "error", 10 | "no-unused-vars": "off", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .env -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Colyseus! 2 | 3 | This project has been created using [⚔️ `create-colyseus-app`](https://github.com/colyseus/create-colyseus-app/) - an npm init template for kick starting a Colyseus project in TypeScript. 4 | 5 | [Documentation](http://docs.colyseus.io/) 6 | 7 | ## :crossed_swords: Usage 8 | 9 | ``` 10 | npm start 11 | ``` 12 | 13 | ## Structure 14 | 15 | - `index.ts`: main entry point, register an empty room handler and attach [`@colyseus/monitor`](https://github.com/colyseus/colyseus-monitor) 16 | - `src/rooms/MyRoom.ts`: an empty room handler for you to implement your logic 17 | - `src/rooms/schema/MyRoomState.ts`: an empty schema used on your room's state. 18 | - `loadtest/example.ts`: scriptable client for the loadtest tool (see `npm run loadtest`) 19 | - `package.json`: 20 | - `scripts`: 21 | - `npm start`: runs `ts-node-dev index.ts` 22 | - `npm test`: runs mocha test suite 23 | - `npm run loadtest`: runs the [`@colyseus/loadtest`](https://github.com/colyseus/colyseus-loadtest/) tool for testing the connection, using the `loadtest/example.ts` script. 24 | - `tsconfig.json`: TypeScript configuration file 25 | 26 | 27 | ## License 28 | 29 | MIT 30 | -------------------------------------------------------------------------------- /server/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P3ntest/zombies-multiplayer/4169f03eff634bf341defa9e49c5da26dba2642f/server/bun.lockb -------------------------------------------------------------------------------- /server/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | /** 4 | * COLYSEUS CLOUD WARNING: 5 | * ---------------------- 6 | * PLEASE DO NOT UPDATE THIS FILE MANUALLY AS IT MAY CAUSE DEPLOYMENT ISSUES 7 | */ 8 | 9 | module.exports = { 10 | apps : [{ 11 | name: "colyseus-app", 12 | script: 'build/index.js', 13 | time: true, 14 | watch: false, 15 | instances: os.cpus().length, 16 | exec_mode: 'fork', 17 | wait_ready: true, 18 | env_production: { 19 | NODE_ENV: 'production' 20 | } 21 | }], 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /server/loadtest/example.ts: -------------------------------------------------------------------------------- 1 | import { Client, Room } from "colyseus.js"; 2 | import { cli, Options } from "@colyseus/loadtest"; 3 | 4 | export async function main(options: Options) { 5 | const client = new Client(options.endpoint); 6 | const room: Room = await client.joinOrCreate(options.roomName, { 7 | // your join options here... 8 | }); 9 | 10 | console.log("joined successfully!"); 11 | 12 | room.onMessage("message-type", (payload) => { 13 | // logic 14 | }); 15 | 16 | room.onStateChange((state) => { 17 | console.log("state change:", state); 18 | }); 19 | 20 | room.onLeave((code) => { 21 | console.log("left"); 22 | }); 23 | } 24 | 25 | cli(main); 26 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-app", 4 | "version": "1.0.0", 5 | "description": "npm init template for bootstrapping an empty Colyseus project", 6 | "main": "build/index.js", 7 | "engines": { 8 | "node": ">= 16.13.0" 9 | }, 10 | "scripts": { 11 | "start": "tsx watch src/index.ts", 12 | "loadtest": "tsx loadtest/example.ts --room my_room --numClients 2", 13 | "build": "npm run clean && tsc", 14 | "clean": "rimraf build", 15 | "test": "mocha -r tsx test/**_test.ts --exit --timeout 15000", 16 | "start:prod": "tsx src/index.ts", 17 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --fix" 18 | }, 19 | "author": "", 20 | "license": "UNLICENSED", 21 | "bugs": { 22 | "url": "https://github.com/colyseus/create-colyseus/issues" 23 | }, 24 | "homepage": "https://github.com/colyseus/create-colyseus#readme", 25 | "devDependencies": { 26 | "@colyseus/loadtest": "^0.15.0", 27 | "@colyseus/testing": "^0.15.0", 28 | "@types/compression": "^1.7.5", 29 | "@types/express": "^4.17.1", 30 | "@types/express-fileupload": "^1.5.0", 31 | "@types/mocha": "^10.0.1", 32 | "@typescript-eslint/parser": "^7.7.1", 33 | "eslint": "^8.57.0", 34 | "eslint-plugin-unused-imports": "^3.1.0", 35 | "mocha": "^10.2.0", 36 | "prisma": "^5.13.0", 37 | "rimraf": "^5.0.0", 38 | "tsx": "^3.12.6", 39 | "typescript": "^5.2.2" 40 | }, 41 | "dependencies": { 42 | "@colyseus/monitor": "^0.15.0", 43 | "@colyseus/playground": "^0.15.3", 44 | "@colyseus/tools": "^0.15.0", 45 | "@prisma/client": "^5.13.0", 46 | "@trpc/server": "next", 47 | "axios": "^1.6.8", 48 | "colyseus": "^0.15.0", 49 | "compression": "^1.7.4", 50 | "express": "^4.18.2", 51 | "express-fileupload": "^1.5.0", 52 | "jose": "^5.2.4", 53 | "zod": "^3.23.4" 54 | } 55 | } -------------------------------------------------------------------------------- /server/prisma/migrations/20240423202723_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | 6 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 7 | ); 8 | -------------------------------------------------------------------------------- /server/prisma/migrations/20240425202946_add_maps/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Map" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "level" JSONB NOT NULL, 6 | "verified" BOOLEAN NOT NULL DEFAULT false, 7 | "authorId" TEXT NOT NULL, 8 | 9 | CONSTRAINT "Map_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Map" ADD CONSTRAINT "Map_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /server/prisma/migrations/20240426162156_permissions_and_more_map_info/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `updatedAt` to the `Map` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Map" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | ADD COLUMN "plays" INTEGER NOT NULL DEFAULT 0, 10 | ADD COLUMN "published" BOOLEAN NOT NULL DEFAULT false, 11 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 12 | 13 | -- AlterTable 14 | ALTER TABLE "User" ADD COLUMN "scopePermissions" TEXT[]; 15 | -------------------------------------------------------------------------------- /server/prisma/migrations/20240428115223_add_custom_assets/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Map" ALTER COLUMN "updatedAt" DROP DEFAULT; 3 | 4 | -- CreateTable 5 | CREATE TABLE "CustomAsset" ( 6 | "id" TEXT NOT NULL, 7 | "uploadId" TEXT NOT NULL, 8 | "name" TEXT NOT NULL, 9 | "description" TEXT, 10 | "tags" TEXT[], 11 | "verified" BOOLEAN NOT NULL DEFAULT false, 12 | "uploadedById" TEXT, 13 | 14 | CONSTRAINT "CustomAsset_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "CustomAsset" ADD CONSTRAINT "CustomAsset_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 19 | -------------------------------------------------------------------------------- /server/prisma/migrations/20240429115754_add_played_games/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `plays` on the `Map` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Map" DROP COLUMN "plays"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "PlayedGame" ( 12 | "id" TEXT NOT NULL, 13 | "mapId" TEXT NOT NULL, 14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "highestWaveSurvived" INTEGER NOT NULL, 16 | 17 | CONSTRAINT "PlayedGame_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "PlayedGameParticipant" ( 22 | "id" TEXT NOT NULL, 23 | "playedGameId" TEXT NOT NULL, 24 | "userId" TEXT NOT NULL, 25 | "kills" INTEGER NOT NULL, 26 | "deaths" INTEGER NOT NULL, 27 | "accuracy" DOUBLE PRECISION NOT NULL, 28 | "wavesSurvived" INTEGER NOT NULL, 29 | "damageDealt" INTEGER NOT NULL, 30 | "score" INTEGER NOT NULL, 31 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 32 | 33 | CONSTRAINT "PlayedGameParticipant_pkey" PRIMARY KEY ("id") 34 | ); 35 | 36 | -- AddForeignKey 37 | ALTER TABLE "PlayedGame" ADD CONSTRAINT "PlayedGame_mapId_fkey" FOREIGN KEY ("mapId") REFERENCES "Map"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 38 | 39 | -- AddForeignKey 40 | ALTER TABLE "PlayedGameParticipant" ADD CONSTRAINT "PlayedGameParticipant_playedGameId_fkey" FOREIGN KEY ("playedGameId") REFERENCES "PlayedGame"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 41 | 42 | -- AddForeignKey 43 | ALTER TABLE "PlayedGameParticipant" ADD CONSTRAINT "PlayedGameParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 44 | -------------------------------------------------------------------------------- /server/prisma/migrations/20240429152723_add_username_to_participant/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "PlayedGameParticipant" DROP CONSTRAINT "PlayedGameParticipant_userId_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "PlayedGameParticipant" ADD COLUMN "username" TEXT NOT NULL DEFAULT 'Anonymous', 6 | ALTER COLUMN "userId" DROP NOT NULL; 7 | 8 | -- AddForeignKey 9 | ALTER TABLE "PlayedGameParticipant" ADD CONSTRAINT "PlayedGameParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 10 | -------------------------------------------------------------------------------- /server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id String @id 18 | name String 19 | 20 | maps Map[] 21 | scopePermissions String[] 22 | uploadedCustomAssets CustomAsset[] 23 | playedGames PlayedGameParticipant[] 24 | } 25 | 26 | model PlayedGame { 27 | id String @id @default(cuid()) 28 | 29 | mapId String 30 | map Map @relation(fields: [mapId], references: [id]) 31 | 32 | createdAt DateTime @default(now()) 33 | highestWaveSurvived Int 34 | participants PlayedGameParticipant[] 35 | } 36 | 37 | model PlayedGameParticipant { 38 | id String @id @default(cuid()) 39 | 40 | playedGameId String 41 | playedGame PlayedGame @relation(fields: [playedGameId], references: [id]) 42 | 43 | userId String? 44 | user User? @relation(fields: [userId], references: [id]) 45 | username String @default("Anonymous") 46 | 47 | kills Int 48 | deaths Int 49 | accuracy Float 50 | wavesSurvived Int 51 | damageDealt Int 52 | score Int 53 | 54 | createdAt DateTime @default(now()) 55 | } 56 | 57 | model Map { 58 | id String @id @default(cuid()) 59 | name String 60 | level Json 61 | 62 | verified Boolean @default(false) 63 | published Boolean @default(false) 64 | 65 | plays PlayedGame[] 66 | 67 | createdAt DateTime @default(now()) 68 | updatedAt DateTime @updatedAt 69 | 70 | authorId String 71 | author User @relation(fields: [authorId], references: [id]) 72 | } 73 | 74 | model CustomAsset { 75 | id String @id @default(cuid()) 76 | uploadId String 77 | name String 78 | description String? 79 | tags String[] 80 | 81 | verified Boolean @default(false) 82 | 83 | uploadedBy User? @relation(fields: [uploadedById], references: [id]) 84 | uploadedById String? 85 | } 86 | -------------------------------------------------------------------------------- /server/src/app.config.ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from "./trpc/router"; 2 | import config from "@colyseus/tools"; 3 | import { monitor } from "@colyseus/monitor"; 4 | import { playground } from "@colyseus/playground"; 5 | import express from "express"; 6 | import { join } from "path"; 7 | import compression from "compression"; 8 | import * as trpcExpress from "@trpc/server/adapters/express"; 9 | import fileUpload from "express-fileupload"; 10 | /** 11 | * Import your Room files 12 | */ 13 | import { MyRoom } from "./rooms/MyRoom"; 14 | import { createContext, extractUserFromRequest } from "./trpc/context"; 15 | import { handleAssetUpload } from "./trpc/assetRouter"; 16 | 17 | export default config({ 18 | initializeGameServer: (gameServer) => { 19 | /** 20 | * Define your room handlers: 21 | */ 22 | gameServer.define("my_room", MyRoom); 23 | }, 24 | 25 | initializeExpress: (app) => { 26 | /** 27 | * Bind your custom express routes here: 28 | * Read more: https://expressjs.com/en/starter/basic-routing.html 29 | */ 30 | if (process.env.NODE_ENV !== "production") { 31 | app.use("/playground", playground); 32 | } else { 33 | const clientBuildPath = join(__dirname, "..", "client", "dist"); 34 | console.log("clientBuildPath", clientBuildPath); 35 | app.use("/", compression(), express.static(clientBuildPath)); 36 | app.get("/auth/callback", (req, res) => { 37 | res.sendFile(join(clientBuildPath, "index.html")); 38 | }); 39 | } 40 | 41 | app.use( 42 | "/trpc", 43 | trpcExpress.createExpressMiddleware({ 44 | router: appRouter, 45 | createContext, 46 | }) 47 | ); 48 | 49 | app.post( 50 | "/createAsset", 51 | fileUpload({ 52 | limits: { fileSize: 10 * 1024 * 1024 }, // 10MB 53 | }), 54 | async (req, res) => { 55 | console.log("uploading file"); 56 | const user = await extractUserFromRequest(req); 57 | if (!user) { 58 | return res.status(401).send("Unauthorized"); 59 | } 60 | await handleAssetUpload(user.user.id, req.files?.file as any, { 61 | name: req.body.assetName, 62 | }); 63 | res.send("ok"); 64 | } 65 | ); 66 | 67 | /** 68 | * Use @colyseus/monitor 69 | * It is recommended to protect this route with a password 70 | * Read more: https://docs.colyseus.io/tools/monitor/#restrict-access-to-the-panel-using-a-password 71 | */ 72 | app.use("/colyseus", monitor()); 73 | }, 74 | 75 | beforeListen: () => { 76 | /** 77 | * Before before gameServer.listen() is called. 78 | */ 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /server/src/game/console/commandHandler.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "colyseus"; 2 | import { MyRoom } from "../../rooms/MyRoom"; 3 | 4 | export function handleCommand(room: MyRoom, client: Client, message: string) { 5 | const command = message.split(" ")[0].substring(1); 6 | 7 | const canDev = () => { 8 | if (process.env.NODE_ENV === "development") return true; 9 | room.sendChatToPlayer( 10 | client.id, 11 | "This command is only available in development mode", 12 | "red" 13 | ); 14 | return false; 15 | }; 16 | 17 | switch (command) { 18 | case "help": 19 | room.sendChatToPlayer( 20 | client.id, 21 | "You really thought I would help you? You're on your own.", 22 | "green" 23 | ); 24 | break; 25 | case "/money": // commands starting with two slashes are for cheating and only in development 26 | if (!canDev()) return; 27 | room.state.players.get(client.id).skillPoints += 1000; 28 | break; 29 | case "/spawn": { 30 | if (!canDev()) return; 31 | const spawnType = message.split(" ")[1] || "normal"; 32 | switch (spawnType) { 33 | case "normal": 34 | case "tank": 35 | case "baby": 36 | case "greenMutant": 37 | case "mutatedBaby": 38 | case "blueMutant": 39 | room.requestSpawnZombie(undefined, spawnType); 40 | break; 41 | default: 42 | room.sendChatToPlayer(client.id, "Unknown spawn type", "red"); 43 | } 44 | break; 45 | } 46 | case "suicide": 47 | room.killPlayer(client.id); 48 | break; 49 | default: 50 | room.sendChatToPlayer( 51 | client.id, 52 | "Unknown command. Use /help for a list of available commands", 53 | "red" 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/src/game/mapEditor/editorTypes.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // a type that accepts strings but 4 | const FloatLike = z.union([z.string(), z.number()]).transform((val, ctx) => { 5 | if (typeof val === "string") { 6 | const num = parseFloat(val); 7 | if (isNaN(num)) { 8 | ctx.addIssue({ 9 | code: z.ZodIssueCode.custom, 10 | message: "Expected a number", 11 | }); 12 | return z.NEVER; 13 | } 14 | return num; 15 | } 16 | return val; 17 | }); 18 | 19 | const IntLike = FloatLike.transform((n) => Math.round(n)); 20 | 21 | const Transform = z.object({ 22 | x: IntLike, 23 | y: IntLike, 24 | scale: FloatLike, 25 | rotation: FloatLike, 26 | }); 27 | 28 | export const ColliderShape = z.discriminatedUnion("shape", [ 29 | z.object({ 30 | shape: z.literal("circle"), 31 | radius: IntLike, 32 | }), 33 | z.object({ 34 | shape: z.literal("rectangle"), 35 | width: IntLike, 36 | height: IntLike, 37 | }), 38 | ]); 39 | 40 | export const AssetCollider = Transform.omit({ 41 | scale: true, 42 | }).extend({ 43 | shape: ColliderShape, 44 | destroyBullet: z.boolean().default(true), 45 | }); 46 | 47 | export const BuiltInAsset = z.object({ 48 | assetSource: z.literal("builtIn"), 49 | assetPath: z.string(), 50 | }); 51 | 52 | export const CustomAsset = z.object({ 53 | assetSource: z.literal("custom"), 54 | uploadId: z.string(), 55 | }); 56 | 57 | // export const ExternalAsset = z.object({ 58 | // assetSource: z.literal("external"), 59 | // assetUrl: z.string(), 60 | // }); 61 | 62 | export const AssetSource = z.discriminatedUnion("assetSource", [ 63 | BuiltInAsset, 64 | CustomAsset, 65 | // ExternalAsset, 66 | ]); 67 | 68 | export const AssetObject = Transform.extend({ 69 | objectType: z.literal("asset"), 70 | colliders: z.array(AssetCollider), 71 | sprite: AssetSource, 72 | id: z.string(), 73 | 74 | zHeight: IntLike.default(0), 75 | 76 | shadow: z 77 | .object({ 78 | enabled: z.boolean(), 79 | offset: IntLike, 80 | }) 81 | .default({ enabled: false, offset: 0 }), 82 | 83 | tiling: z.boolean(), 84 | width: IntLike, 85 | height: IntLike, 86 | }); 87 | 88 | export const SpawnPoint = Transform.extend({ 89 | objectType: z.literal("spawnPoint"), 90 | spawns: z.enum(["player", "zombie"]), 91 | id: z.string(), 92 | }); 93 | 94 | export const MapObject = z.discriminatedUnion("objectType", [ 95 | AssetObject, 96 | SpawnPoint, 97 | ]); 98 | 99 | export const GameLevel = z.object({ 100 | objects: z.array(MapObject), 101 | }); 102 | 103 | export type GameLevel = z.infer; 104 | export type MapObject = z.infer; 105 | export type AssetObject = z.infer; 106 | export type SpawnPoint = z.infer; 107 | export type AssetCollider = z.infer; 108 | export type ColliderShape = z.infer; 109 | export type BuiltInAsset = z.infer; 110 | export type CustomAsset = z.infer; 111 | // export type ExternalAsset = z.infer; 112 | export type AssetSource = z.infer; 113 | -------------------------------------------------------------------------------- /server/src/game/maps.ts: -------------------------------------------------------------------------------- 1 | export const maps = ["dust3"]; 2 | export type MapID = (typeof maps)[number]; 3 | -------------------------------------------------------------------------------- /server/src/game/player.ts: -------------------------------------------------------------------------------- 1 | import { PlayerState } from "../rooms/schema/MyRoomState"; 2 | import { callWaveBasedFunction, playerConfig, weaponConfig } from "./config"; 3 | 4 | export type PlayerClass = "pistol" | "shotgun" | "rifle" | "melee"; 5 | 6 | export const PlayerAnimations = { 7 | NONE: 0, 8 | RELOAD: 1, 9 | MELEE: 2, 10 | }; 11 | 12 | export function calculateScore(player: PlayerState) { 13 | return ( 14 | player.kills * 100 + 15 | player.damageDealt + 16 | player.wavesSurvived * 100 + 17 | player.accuracy * 100 18 | ); 19 | } 20 | 21 | export function getWeaponData(playerClass: PlayerClass) { 22 | return weaponConfig.weapons[playerClass]; 23 | } 24 | 25 | export function getMaxHealth(player: PlayerState) { 26 | return callWaveBasedFunction( 27 | playerConfig.healthUpgrade, 28 | player.upgrades.health, 29 | playerConfig.startingHealth 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /server/src/game/waves.ts: -------------------------------------------------------------------------------- 1 | import { SpawnChance, callWaveBasedFunction, waveConfig } from "./config"; 2 | import { ZombieType } from "./zombies"; 3 | 4 | export function generateWave(wave: number, players: number = 1) { 5 | wave--; // 0-indexed 6 | 7 | return { 8 | wave, 9 | zombies: Math.round(callWaveBasedFunction(waveConfig.zombies, wave)), 10 | zombieSpawnInterval: Math.max( 11 | waveConfig.zombieSpawnInterval.max, 12 | waveConfig.zombieSpawnInterval.base + 13 | waveConfig.zombieSpawnInterval.factor * wave 14 | ), 15 | zombieHealthMultiplier: callWaveBasedFunction( 16 | waveConfig.zombieHealthMultiplier, 17 | wave * players 18 | ), 19 | zombieAttackMultiplier: callWaveBasedFunction( 20 | waveConfig.zombieAttackMultiplier, 21 | wave 22 | ), 23 | spawnChances: { 24 | normal: calcSpawnChange(wave, "normal"), 25 | baby: calcSpawnChange(wave, "baby"), 26 | mutatedBaby: calcSpawnChange(wave, "mutatedBaby"), 27 | greenMutant: calcSpawnChange(wave, "greenMutant"), 28 | tank: calcSpawnChange(wave, "tank"), 29 | blueMutant: calcSpawnChange(wave, "blueMutant"), 30 | }, 31 | postDelay: waveConfig.postDelay, 32 | }; 33 | } 34 | 35 | export function calculateZombieSpawnType(wave: number): ZombieType { 36 | const spawnChances = generateWave(wave).spawnChances; 37 | 38 | const total = Object.values(spawnChances).reduce((a, b) => a + b, 0); 39 | 40 | let random = Math.random() * total; 41 | for (const [type, chance] of Object.entries(spawnChances)) { 42 | random -= chance; 43 | if (random <= 0) return type as ZombieType; 44 | } 45 | } 46 | 47 | function calcSpawnChange(wave: number, type: ZombieType) { 48 | const spawnChance = waveConfig.spawnChances[type] as SpawnChance; 49 | const chance = minmax( 50 | (wave + 1) * spawnChance.factor + spawnChance.base, // wave + 1 because wave is 0-indexed 51 | spawnChance.base, // min 52 | spawnChance.max // max 53 | ); 54 | return chance; 55 | } 56 | 57 | // for (let wave = 0; wave < 20; wave++) { 58 | // console.log(`============ Wave ${wave + 1} ============`); 59 | // // log how what types of zombies spawn 60 | // const numZombies = generateWave(wave).zombies; 61 | // const zombieSpawns: Record = { 62 | // normal: 0, 63 | // baby: 0, 64 | // mutatedBaby: 0, 65 | // greenMutant: 0, 66 | // tank: 0, 67 | // blueMutant: 0, 68 | // }; 69 | // for (let i = 0; i < numZombies; i++) { 70 | // zombieSpawns[calculateZombieSpawnType(wave)]++; 71 | // } 72 | // console.log(zombieSpawns); 73 | // } 74 | 75 | function minmax(value: number, min: number, max: number) { 76 | return Math.max(min, Math.min(max, value)); 77 | } 78 | -------------------------------------------------------------------------------- /server/src/game/zombies.ts: -------------------------------------------------------------------------------- 1 | export type ZombieType = 2 | | "normal" 3 | | "baby" 4 | | "greenMutant" 5 | | "tank" 6 | | "mutatedBaby" 7 | | "blueMutant"; 8 | 9 | export const zombieInfo: Record< 10 | ZombieType, 11 | { 12 | baseHealth: number; 13 | baseSpeed: number; 14 | baseAttackDamage: number; 15 | size: number; 16 | attackDelayTicks?: number; 17 | tint?: string | number; 18 | glow?: number; 19 | } 20 | > = { 21 | normal: { 22 | baseHealth: 100, 23 | baseSpeed: 1, 24 | baseAttackDamage: 10, 25 | size: 1, 26 | }, 27 | baby: { 28 | baseHealth: 30, 29 | baseSpeed: 3, 30 | baseAttackDamage: 10, 31 | attackDelayTicks: 0, 32 | size: 0.7, 33 | }, 34 | mutatedBaby: { 35 | baseHealth: 50, 36 | baseSpeed: 3.5, 37 | baseAttackDamage: 20, 38 | attackDelayTicks: 0, 39 | size: 0.7, 40 | tint: 0xccccff, 41 | glow: 0x0000ff, 42 | }, 43 | greenMutant: { 44 | baseHealth: 200, 45 | baseSpeed: 1, 46 | baseAttackDamage: 20, 47 | size: 1, 48 | tint: 0x00ff00, 49 | glow: 0x00ff00, 50 | }, 51 | tank: { 52 | baseHealth: 600, 53 | baseSpeed: 0.8, 54 | baseAttackDamage: 110, 55 | size: 1.9, 56 | }, 57 | blueMutant: { 58 | baseHealth: 300, 59 | baseSpeed: 1.3, 60 | baseAttackDamage: 30, 61 | size: 1.4, 62 | tint: 0x8888ff, 63 | }, 64 | } as const; 65 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * IMPORTANT: 3 | * --------- 4 | * Do not manually edit this file if you'd like to host your server on Colyseus Cloud 5 | * 6 | * If you're self-hosting (without Colyseus Cloud), you can manually 7 | * instantiate a Colyseus Server as documented here: 8 | * 9 | * See: https://docs.colyseus.io/server/api/#constructor-options 10 | */ 11 | import { listen } from "@colyseus/tools"; 12 | import "./prisma"; 13 | 14 | // Import Colyseus config 15 | import app from "./app.config"; 16 | 17 | // Create and listen on 2567 (or PORT environment variable.) 18 | listen(app); 19 | -------------------------------------------------------------------------------- /server/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prisma = new PrismaClient(); 4 | 5 | // create a new user 6 | prisma.user.count().then((count) => { 7 | console.log(`There are ${count} users in the database`); 8 | }); 9 | -------------------------------------------------------------------------------- /server/src/rooms/schema/MyRoomState.ts: -------------------------------------------------------------------------------- 1 | import { ArraySchema, MapSchema, Schema, type } from "@colyseus/schema"; 2 | import { PlayerClass } from "../../game/player"; 3 | import { ZombieType } from "../../game/zombies"; 4 | import { playerConfig } from "../../game/config"; 5 | 6 | export const PlayerHealthState = { 7 | ALIVE: 0, 8 | DEAD: 1, 9 | NOT_SPAWNED: 2, 10 | }; 11 | 12 | export class PlayerUpgradeState extends Schema { 13 | @type("uint8") fireRate: number = 0; 14 | @type("uint8") damage: number = 0; 15 | @type("uint8") pierce: number = 0; 16 | @type("uint8") health: number = 0; 17 | @type("uint8") speed: number = 0; 18 | @type("uint8") scope: number = 0; 19 | } 20 | 21 | export class PlayerState extends Schema { 22 | @type("string") name: string = "Unnamed"; 23 | @type("string") sessionId: string = ""; 24 | 25 | @type("int32") x: number = 0; 26 | @type("int32") y: number = 0; 27 | @type("float32") rotation: number = 0; 28 | 29 | @type("boolean") connected: boolean = true; 30 | 31 | @type("float32") velocityX: number = 0; 32 | @type("float32") velocityY: number = 0; 33 | 34 | @type("uint32") health: number = playerConfig.startingHealth; 35 | @type("uint8") healthState: number = PlayerHealthState.NOT_SPAWNED; 36 | 37 | @type("uint32") skillPoints: number = 0; 38 | 39 | @type("string") playerClass: PlayerClass = "pistol"; 40 | 41 | @type(PlayerUpgradeState) upgrades = new PlayerUpgradeState(); 42 | 43 | @type("uint32") kills: number = 0; 44 | @type("uint32") deaths: number = 0; 45 | @type("uint32") damageDealt: number = 0; 46 | @type("uint32") wavesSurvived: number = 0; 47 | @type("uint32") accuracy: number = 0; 48 | 49 | @type("uint8") currentAnimation = 0; 50 | 51 | @type("boolean") finishedLoading = false; 52 | } 53 | 54 | export class ZombieState extends Schema { 55 | @type("uint32") id: number = 0; 56 | @type("int32") x: number = 0; 57 | @type("int32") y: number = 0; 58 | @type("float32") rotation: number = 0; 59 | @type("string") playerId: string = ""; 60 | @type("uint32") health: number = 100; 61 | @type("uint32") maxHealth: number = 100; 62 | @type("string") targetPlayerId: string = ""; 63 | 64 | @type("uint32") lastAttackTick: number = 0; 65 | @type("uint32") attackCoolDownTicks: number = 20; 66 | 67 | @type("string") zombieType: ZombieType = "normal"; 68 | } 69 | 70 | export class BulletState extends Schema { 71 | @type("uint32") id: number = 0; 72 | @type("string") playerId: string = ""; 73 | @type("int32") originX: number = 0; 74 | @type("int32") originY: number = 0; 75 | @type("float32") rotation: number = 0; 76 | @type("float32") speed: number = 0; 77 | @type("uint32") damage: number = 0; 78 | @type("uint8") piercesLeft: number = 0; 79 | @type("float32") knockBack: number = 1; 80 | } 81 | 82 | export class WaveInfoState extends Schema { 83 | @type("uint32") currentWaveNumber: number = 0; 84 | @type("boolean") active: boolean = false; 85 | @type("uint16") nextWaveStartsInSec: number = 0; 86 | @type("uint32") totalZombies: number = 0; 87 | @type("uint32") zombiesLeft: number = 0; 88 | } 89 | 90 | export class MyRoomState extends Schema { 91 | @type({ 92 | map: PlayerState, 93 | }) 94 | players = new MapSchema(); 95 | 96 | @type({ 97 | array: BulletState, 98 | }) 99 | bullets = new ArraySchema(); 100 | 101 | @type({ 102 | array: ZombieState, 103 | }) 104 | zombies = new ArraySchema(); 105 | 106 | @type("uint32") 107 | gameTick = 0; 108 | 109 | @type(WaveInfoState) 110 | waveInfo = new WaveInfoState(); 111 | 112 | @type("boolean") 113 | isGameOver = false; 114 | 115 | @type("string") 116 | mapId: string = ""; 117 | } 118 | -------------------------------------------------------------------------------- /server/src/trpc/context.ts: -------------------------------------------------------------------------------- 1 | import { CreateExpressContextOptions } from "@trpc/server/adapters/express"; 2 | import { createRemoteJWKSet, jwtVerify } from "jose"; 3 | import { prisma } from "../prisma"; 4 | // eslint-disable-next-line no-redeclare 5 | import { Request } from "express"; 6 | 7 | const jwks = createRemoteJWKSet( 8 | new URL("https://zombies-auth.p3ntest.dev/oidc/jwks") 9 | ); 10 | 11 | export async function getUserForToken(token: string) { 12 | if (!token) return null; 13 | const { payload } = await jwtVerify(token, jwks, { 14 | // Expected issuer of the token, issued by the Logto server 15 | issuer: "https://zombies-auth.p3ntest.dev/oidc", 16 | // Expected audience token, the resource indicator of the current API 17 | audience: "https://apocalypse.p3ntest.dev/", 18 | }); 19 | 20 | const { sub } = payload; 21 | const scopePermissions = ((payload.scope as string) ?? "").split(" "); 22 | 23 | return await prisma.user.upsert({ 24 | where: { 25 | id: sub, 26 | }, 27 | update: { 28 | scopePermissions: { 29 | set: scopePermissions, 30 | }, 31 | }, 32 | create: { 33 | id: sub, 34 | name: "Anonymous", 35 | scopePermissions: { 36 | set: scopePermissions, 37 | }, 38 | }, 39 | }); 40 | } 41 | 42 | export async function extractUserFromRequest(req: Request) { 43 | if (!req.headers.authorization) { 44 | return null; 45 | } 46 | if (!req.headers.authorization.startsWith("Bearer ")) { 47 | return null; 48 | } 49 | const token = req.headers.authorization.replace("Bearer ", ""); 50 | 51 | const user = await getUserForToken(token); 52 | 53 | return { 54 | user, 55 | }; 56 | } 57 | 58 | export const createContext = async (opts: CreateExpressContextOptions) => { 59 | return await extractUserFromRequest(opts.req); 60 | }; 61 | -------------------------------------------------------------------------------- /server/src/trpc/router.ts: -------------------------------------------------------------------------------- 1 | import { mapRouter } from "./mapRouter"; 2 | import { statRouter } from "./statRouter"; 3 | import { authProcedure, router } from "./trpc"; 4 | 5 | export const appRouter = router({ 6 | testConnection: authProcedure.mutation(() => { 7 | return "Connection Test Successful!"; 8 | }), 9 | maps: mapRouter, 10 | stats: statRouter, 11 | }); 12 | 13 | // Export type router type signature, 14 | // NOT the router itself. 15 | export type AppRouter = typeof appRouter; 16 | -------------------------------------------------------------------------------- /server/src/trpc/statRouter.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../prisma"; 2 | import { publicProcedure, router } from "./trpc"; 3 | 4 | export const statRouter = router({ 5 | getLeaderboard: publicProcedure.query(async () => { 6 | const leaderboard = await prisma.playedGameParticipant.findMany({ 7 | take: 10, 8 | orderBy: { 9 | score: "desc", 10 | }, 11 | include: { 12 | playedGame: { 13 | include: { 14 | map: true, 15 | participants: true, 16 | }, 17 | }, 18 | }, 19 | }); 20 | return { 21 | leaderboard, 22 | }; 23 | }), 24 | }); 25 | -------------------------------------------------------------------------------- /server/src/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from "@trpc/server"; 2 | import { createContext } from "./context"; 3 | 4 | const t = initTRPC.context().create(); 5 | 6 | export const router = t.router; 7 | export const publicProcedure = t.procedure; 8 | export const authProcedure = publicProcedure.use(({ ctx, next }) => { 9 | if (!ctx?.user) { 10 | throw new Error("Unauthorized"); 11 | } 12 | return next(); 13 | }); 14 | -------------------------------------------------------------------------------- /server/src/util.ts: -------------------------------------------------------------------------------- 1 | export function genId() { 2 | return Math.random().toString(36).substring(7); 3 | } 4 | -------------------------------------------------------------------------------- /server/test/MyRoom_test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { ColyseusTestServer, boot } from "@colyseus/testing"; 3 | 4 | // import your "app.config.ts" file here. 5 | import appConfig from "../src/app.config"; 6 | import { MyRoomState } from "../src/rooms/schema/MyRoomState"; 7 | 8 | describe("testing your Colyseus app", () => { 9 | let colyseus: ColyseusTestServer; 10 | 11 | before(async () => colyseus = await boot(appConfig)); 12 | after(async () => colyseus.shutdown()); 13 | 14 | beforeEach(async () => await colyseus.cleanup()); 15 | 16 | it("connecting into a room", async () => { 17 | // `room` is the server-side Room instance reference. 18 | const room = await colyseus.createRoom("my_room", {}); 19 | 20 | // `client1` is the client-side `Room` instance reference (same as JavaScript SDK) 21 | const client1 = await colyseus.connectTo(room); 22 | 23 | // make your assertions 24 | assert.strictEqual(client1.sessionId, room.clients[0].sessionId); 25 | 26 | // wait for state sync 27 | await room.waitForNextPatch(); 28 | 29 | assert.deepStrictEqual({ mySynchronizedProperty: "Hello world" }, client1.state.toJSON()); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "allowJs": true, 9 | "strictNullChecks": false, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "useDefineForClassFields": false 15 | }, 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | bunx prisma migrate deploy 3 | node build/index.js --------------------------------------------------------------------------------