├── 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 | Name |
29 | Kills |
30 | Deaths |
31 | Accuracy |
32 | Waves Survived |
33 | Damage |
34 | Score |
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 | {player.name} |
48 | {player.kills} |
49 | {player.deaths} |
50 | {player.accuracy} |
51 | {player.wavesSurvived} |
52 | {player.damageDealt} |
53 | {player.score} |
54 |
55 | ))}
56 |
57 |
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 |
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
--------------------------------------------------------------------------------