├── apps ├── backend │ ├── .prettierignore │ ├── .prettierrc │ ├── .eslintrc │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ ├── store.ts │ │ └── index.ts │ ├── LICENSE │ └── yarn.lock ├── example │ ├── .prettierignore │ ├── src │ │ ├── routes │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ │ ├── app.css │ │ ├── app.d.ts │ │ └── app.html │ ├── .prettierrc │ ├── tailwind.config.cjs │ ├── vite.config.mjs │ ├── tsconfig.json │ ├── postcss.config.mjs │ ├── svelte.config.js │ ├── .eslintrc.cjs │ ├── package.json │ └── .gitignore └── frontend │ ├── .prettierignore │ ├── src │ ├── routes │ │ ├── +layout.svelte │ │ ├── (play) │ │ │ ├── +layout.server.ts │ │ │ ├── +page.svelte │ │ │ ├── +layout.svelte │ │ │ └── Chat.svelte │ │ └── setup │ │ │ ├── +page.server.ts │ │ │ └── +page@.svelte │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ └── lib │ │ ├── stores.ts │ │ ├── Login.svelte │ │ ├── audio.ts │ │ ├── socket.ts │ │ ├── MyPlayer.svelte │ │ └── Player.svelte │ ├── .prettierrc │ ├── static │ ├── ui.png │ ├── stack.png │ ├── voice.png │ ├── scoreboard.png │ └── _app │ │ └── immutable │ │ ├── a.mp3 │ │ └── a.wav │ ├── tailwind.config.cjs │ ├── vite.config.mjs │ ├── tsconfig.json │ ├── postcss.config.mjs │ ├── .eslintrc.cjs │ ├── svelte.config.js │ ├── package.json │ ├── LICENSE │ └── .gitignore ├── .env.example ├── coturn.conf ├── pnpm-workspace.yaml ├── .dockerignore ├── packages ├── eslint-config-custom │ ├── index.js │ └── package.json └── common │ ├── package.json │ ├── tsconfig.json │ └── src │ └── index.ts ├── .eslintrc.js ├── turbo.json ├── .gitignore ├── nginx.conf ├── package.json ├── Dockerfile ├── README.md └── docker-compose.yml /apps/backend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /apps/example/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /apps/frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | AUTH_TOKEN=password 2 | HOST=example.com 3 | -------------------------------------------------------------------------------- /apps/example/src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | -------------------------------------------------------------------------------- /coturn.conf: -------------------------------------------------------------------------------- 1 | listening-ip=0.0.0.0 2 | listening-port=3478 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/.svelte-kit 4 | Dockerfile 5 | **/.env 6 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["turbo", "prettier"], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/example/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": true 6 | } -------------------------------------------------------------------------------- /apps/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": true 6 | } -------------------------------------------------------------------------------- /apps/frontend/static/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomated/agnostic-proximity/HEAD/apps/frontend/static/ui.png -------------------------------------------------------------------------------- /apps/backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": true 6 | } -------------------------------------------------------------------------------- /apps/frontend/static/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomated/agnostic-proximity/HEAD/apps/frontend/static/stack.png -------------------------------------------------------------------------------- /apps/frontend/static/voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomated/agnostic-proximity/HEAD/apps/frontend/static/voice.png -------------------------------------------------------------------------------- /apps/frontend/src/app.css: -------------------------------------------------------------------------------- 1 | @import '@fontsource/inter'; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; -------------------------------------------------------------------------------- /apps/example/src/app.css: -------------------------------------------------------------------------------- 1 | 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | body { 7 | height: 100vh; 8 | } -------------------------------------------------------------------------------- /apps/frontend/static/scoreboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomated/agnostic-proximity/HEAD/apps/frontend/static/scoreboard.png -------------------------------------------------------------------------------- /apps/frontend/static/_app/immutable/a.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomated/agnostic-proximity/HEAD/apps/frontend/static/_app/immutable/a.mp3 -------------------------------------------------------------------------------- /apps/frontend/static/_app/immutable/a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomated/agnostic-proximity/HEAD/apps/frontend/static/_app/immutable/a.wav -------------------------------------------------------------------------------- /apps/frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace App { 4 | interface Locals { 5 | token: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "main": "src/index.ts", 4 | "types": "src/index.ts", 5 | "type": "module", 6 | "dependencies": { 7 | "zod": "^3.21.4" 8 | } 9 | } -------------------------------------------------------------------------------- /apps/example/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /apps/example/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | 3 | /** @type {import('vite').UserConfig} */ 4 | const config = { 5 | plugins: [sveltekit()], 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/frontend/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /apps/frontend/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | 3 | /** @type {import('vite').UserConfig} */ 4 | const config = { 5 | plugins: [sveltekit()], 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/(play)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | export const prerender = false; 3 | 4 | export const load = async ({ locals }) => { 5 | return { 6 | token: locals.token, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /apps/example/src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare namespace App { 5 | interface Platform { 6 | env: { 7 | // KV: KVNamespace; 8 | }; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "alwaysStrict": true, 6 | "noUncheckedIndexedAccess": true, 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/setup/+page.server.ts: -------------------------------------------------------------------------------- 1 | export const load = async ({ locals }) => { 2 | return { 3 | token: locals.token, 4 | }; 5 | }; 6 | 7 | export const prerender = false; 8 | export const csr = false; 9 | export const ssr = true; 10 | -------------------------------------------------------------------------------- /apps/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "alwaysStrict": true, 6 | "noUncheckedIndexedAccess": true, 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*/"], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/example/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | import tailwind from 'tailwindcss'; 3 | 4 | /** @type {import('postcss-load-config').Config} */ 5 | const config = { 6 | plugins: [tailwind, autoprefixer], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /apps/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | import tailwind from 'tailwindcss'; 3 | 4 | /** @type {import('postcss-load-config').Config} */ 5 | const config = { 6 | plugins: [tailwind, autoprefixer], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /apps/backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:prettier/recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "ignorePatterns": "dist" 13 | } -------------------------------------------------------------------------------- /apps/example/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import preprocess from 'svelte-preprocess'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: [ 7 | preprocess({ 8 | postcss: true, 9 | }), 10 | ], 11 | 12 | kit: { 13 | adapter: adapter(), 14 | }, 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /apps/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | *.pem 14 | 15 | # debug 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # env files 21 | .env 22 | 23 | # output 24 | /dist -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": [ 4 | "**/.env.*local" 5 | ], 6 | "pipeline": { 7 | "build": { 8 | "dependsOn": [ 9 | "^build" 10 | ], 11 | "outputs": [ 12 | "build/**", 13 | "dist/**" 14 | ] 15 | }, 16 | "lint": { 17 | "outputs": [] 18 | }, 19 | "dev": { 20 | "cache": false 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /apps/example/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 | 12 |
13 | %sveltekit.body% 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 | 12 |
13 | %sveltekit.body% 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/(play)/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#await peerPromise then [peer, gameState]} 12 | 13 | {/await} 14 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eslint": "^8.36.0", 8 | "eslint-config-prettier": "^8.7.0", 9 | "eslint-config-turbo": "^0.0.9" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.9.5" 13 | }, 14 | "publishConfig": { 15 | "access": "public" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | import { env } from '$env/dynamic/private'; 3 | 4 | export const handle: Handle = async ({ event, resolve }) => { 5 | const token = event.url.searchParams.get('token'); 6 | if (!token) return new Response('Missing token'); 7 | if (token !== env.AUTH_TOKEN) return new Response('Invalid token'); 8 | 9 | event.locals.token = token; 10 | 11 | return resolve(event); 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .env 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | upstream frontend { 2 | server frontend:3000; 3 | } 4 | 5 | upstream backend { 6 | server backend:9000; 7 | } 8 | 9 | server { 10 | listen 80; 11 | server_name ${SERVER_NAME}; 12 | 13 | location / { 14 | proxy_pass http://frontend; 15 | } 16 | } 17 | 18 | server { 19 | listen 80; 20 | 21 | server_name backend-${SERVER_NAME}; 22 | 23 | location / { 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection "upgrade"; 27 | proxy_read_timeout 864000; 28 | 29 | proxy_pass http://backend; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "allowJs": true, 8 | "baseUrl": ".", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "incremental": true, 13 | "esModuleInterop": true, 14 | "outDir": "dist", 15 | "module": "CommonJS", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true 18 | }, 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "allowJs": true, 8 | "baseUrl": ".", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "incremental": true, 13 | "esModuleInterop": true, 14 | "outDir": "dist", 15 | "module": "CommonJS", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true 18 | }, 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } -------------------------------------------------------------------------------- /apps/example/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | browser: true, 6 | es2017: true, 7 | node: true, 8 | }, 9 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 10 | plugins: ['svelte3', '@typescript-eslint'], 11 | overrides: [ 12 | { 13 | files: ['*.svelte'], 14 | processor: 'svelte3/svelte3', 15 | }, 16 | { 17 | files: ['*.ts'], 18 | extends: ['plugin:@typescript-eslint/recommended'], 19 | }, 20 | ], 21 | settings: { 22 | 'svelte3/typescript': () => require('typescript'), 23 | }, 24 | parserOptions: { 25 | sourceType: 'module', 26 | ecmaVersion: 2020, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /apps/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | browser: true, 6 | es2017: true, 7 | node: true, 8 | }, 9 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 10 | plugins: ['svelte3', '@typescript-eslint'], 11 | overrides: [ 12 | { 13 | files: ['*.svelte'], 14 | processor: 'svelte3/svelte3', 15 | }, 16 | { 17 | files: ['*.ts'], 18 | extends: ['plugin:@typescript-eslint/recommended'], 19 | }, 20 | ], 21 | settings: { 22 | 'svelte3/typescript': () => require('typescript'), 23 | }, 24 | parserOptions: { 25 | sourceType: 'module', 26 | ecmaVersion: 2020, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agnostic-proximity", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev", 12 | "lint": "turbo run lint", 13 | "format": "prettier --write \"**/*.{ts,tsx,svelte,md}\"", 14 | "start:backend": "cd apps/backend && pnpm start", 15 | "start:frontend": "cd apps/frontend && pnpm start" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "workspace:*", 19 | "prettier": "latest", 20 | "turbo": "latest" 21 | }, 22 | "engines": { 23 | "node": ">=14.0.0" 24 | }, 25 | "dependencies": {}, 26 | "packageManager": "pnpm@7.26.3" 27 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-buster-slim AS builder 2 | ARG app 3 | 4 | WORKDIR /app 5 | RUN npm i -g pnpm turbo 6 | 7 | COPY . . 8 | RUN turbo prune --scope=$app --docker 9 | 10 | FROM node:18-buster-slim AS installer 11 | ARG app 12 | 13 | WORKDIR /app 14 | RUN npm i -g pnpm 15 | 16 | COPY .gitignore .gitignore 17 | COPY --from=builder /app/out/json/ . 18 | COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml 19 | COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml 20 | 21 | RUN pnpm i 22 | COPY --from=builder /app/out/full/ . 23 | RUN pnpm turbo run build --filter=$app... 24 | 25 | FROM node:18-buster-slim AS runner 26 | ARG app 27 | WORKDIR /app 28 | RUN npm i -g pnpm 29 | 30 | RUN addgroup --system --gid 1001 nodejs 31 | RUN adduser --system --uid 1001 runner 32 | USER runner 33 | 34 | COPY --from=installer --chown=runner:nodejs /app ./ 35 | 36 | ENV APP $app 37 | CMD pnpm start:${APP} -------------------------------------------------------------------------------- /apps/frontend/src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | import { writable, type Writable } from 'svelte/store'; 3 | import { uneval } from 'devalue'; 4 | 5 | export function storedWritable( 6 | key: string, 7 | initialValue: T | (() => T) 8 | ): Writable { 9 | let initial: T | undefined; 10 | 11 | if (browser) { 12 | const stored = localStorage.getItem(key); 13 | if (stored) { 14 | console.log(key, stored); 15 | initial = (0, eval)(`(${stored})`); 16 | } 17 | } 18 | 19 | if (!initial) { 20 | if (typeof initialValue === 'function') { 21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 22 | // @ts-ignore 23 | initial = initialValue(); 24 | } else { 25 | initial = initialValue; 26 | } 27 | } 28 | 29 | const store = writable(initial); 30 | 31 | store.subscribe((value) => { 32 | if (!browser) return; 33 | localStorage.setItem(key, uneval(value)); 34 | }); 35 | return store; 36 | } 37 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/(play)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if !isChrome} 19 |
22 |

23 | This probably only works on Chrome-based browsers because the others are 24 | cringe when it comes to audio. 25 |

26 |
27 | {/if} 28 | 29 | {#if $myId} 30 | 31 | {:else} 32 | 33 | {/if} 34 | 35 | -------------------------------------------------------------------------------- /apps/frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import preprocess from 'svelte-preprocess'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: [ 7 | preprocess({ 8 | postcss: true, 9 | replace: [ 10 | [ 11 | /import\s*{\s*([^}]+?)\s*,?\s*}\s*from\s+['"]lucide-svelte['"](\s*;)?/gim, 12 | (_, /**@type {string} */ imports) => { 13 | return imports 14 | .split(/\s*,\s*/gim) 15 | .map((s) => { 16 | let icon = s.split(/\s+as\s+/); 17 | let name = icon[1] || icon[0]; 18 | let file = icon[0] 19 | .replace(/[A-Z]/g, (s) => '-' + s.toLowerCase()) 20 | .slice(1); 21 | return `import ${name} from 'lucide-svelte/dist/esm/icons/${file}.svelte';`; 22 | }) 23 | .join(''); 24 | }, 25 | ], 26 | ], 27 | }), 28 | ], 29 | 30 | kit: { 31 | adapter: adapter(), 32 | }, 33 | }; 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Ottomated", 6 | "email": "otto@ottomated.net" 7 | }, 8 | "private": true, 9 | "scripts": { 10 | "dev": "tsx watch src/index.ts", 11 | "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js", 12 | "lint": "eslint --fix .", 13 | "start": "node dist/index.js" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^18.15.0", 17 | "@types/ws": "^8.5.4", 18 | "@typescript-eslint/eslint-plugin": "^5.54.1", 19 | "@typescript-eslint/parser": "^5.54.1", 20 | "esbuild": "^0.17.11", 21 | "eslint": "^8.36.0", 22 | "eslint-config-prettier": "^8.7.0", 23 | "eslint-plugin-prettier": "^4.2.1", 24 | "prettier": "^2.8.3", 25 | "typescript": "^4.9.5" 26 | }, 27 | "dependencies": { 28 | "common": "workspace:*", 29 | "peer": "^1.0.0", 30 | "tsx": "^3.12.4", 31 | "ws": "^8.13.0", 32 | "zod": "^3.21.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Agnostic Proximity 2 | 3 | Voice chat built to hook into any game 4 | 5 | ## Setup 6 | 7 | 1. Set up `.env` 8 | 9 | ```env 10 | AUTH_TOKEN=password # Static authentication token used by users to connect 11 | HOST=proximity.example.com # Where your backend is hosted - i.e. proximity.example.com 12 | ``` 13 | 14 | 2. Set up a VPS 15 | 16 | - Install docker and docker-compose 17 | - Clone this repo 18 | - Run `docker-compose build` and `docker-compose up` 19 | 20 | 3. Set up DNS records 21 | 22 | Use Cloudflare for automatic SSL. Set SSL mode to Flexible. 23 | 24 | If your `HOST` is set to `proximity.example.com`: 25 | 26 | 1. An `A` record on `backend-proximity.example.com` pointing to your server's IP 27 | 2. An `A` record on `proximity.example.com` pointing to your server's IP 28 | 3. An `A` record on `turn-proximity.example.com` pointing to your server's IP **NOT PROXIED THROUGH CLOUDFLARE** 29 | 4. An `AAAA` record on `turn-proximity.example.com` pointing to your server's IPv6 **NOT PROXIED THROUGH CLOUDFLARE** 30 | -------------------------------------------------------------------------------- /apps/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Ottomated" 6 | }, 7 | "scripts": { 8 | "dev": "vite dev --port 9001", 9 | "lint": "eslint --fix . && svelte-check" 10 | }, 11 | "dependencies": { 12 | "common": "workspace:*", 13 | "tailwindcss": "^3.2.4" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-auto": "^2.0.0", 17 | "@sveltejs/kit": "^1.11.0", 18 | "@typescript-eslint/eslint-plugin": "^5.54.1", 19 | "@typescript-eslint/parser": "^5.54.1", 20 | "autoprefixer": "^10.4.14", 21 | "eslint": "^8.36.0", 22 | "eslint-config-prettier": "^8.7.0", 23 | "eslint-plugin-prettier": "^4.2.1", 24 | "eslint-plugin-svelte3": "^4.0.0", 25 | "postcss": "^8.4.21", 26 | "postcss-load-config": "^4.0.1", 27 | "prettier": "^2.8.3", 28 | "prettier-plugin-svelte": "^2.9.0", 29 | "svelte": "^3.56.0", 30 | "svelte-check": "^3.1.2", 31 | "svelte-preprocess": "^5.0.1", 32 | "tslib": "^2.5.0", 33 | "typescript": "~4.9.5", 34 | "vite": "^4.1.1" 35 | }, 36 | "type": "module" 37 | } -------------------------------------------------------------------------------- /apps/backend/src/store.ts: -------------------------------------------------------------------------------- 1 | import type { ZodSchema } from 'zod'; 2 | 3 | export function writable(schema: ZodSchema, initial: T) { 4 | const subscribers: Array<(value: T) => void> = []; 5 | let value = initial; 6 | return { 7 | subscribe: (subscriber: (value: T) => void) => { 8 | subscribers.push(subscriber); 9 | subscriber(value); 10 | return () => { 11 | const index = subscribers.indexOf(subscriber); 12 | if (index !== -1) subscribers.splice(index, 1); 13 | }; 14 | }, 15 | set: (newValue: T) => { 16 | const res = schema.safeParse(newValue); 17 | if (!res.success) { 18 | console.warn(res.error); 19 | return; 20 | } 21 | value = res.data; 22 | subscribers.forEach((subscriber) => subscriber(value)); 23 | }, 24 | update: (fn: (old: T) => T) => { 25 | const res = schema.safeParse(fn(value)); 26 | if (!res.success) { 27 | console.warn(res.error); 28 | return; 29 | } 30 | value = res.data; 31 | subscribers.forEach((subscriber) => subscriber(value)); 32 | }, 33 | get: () => value, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | frontend: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | args: 8 | app: frontend 9 | environment: 10 | - AUTH_TOKEN=${AUTH_TOKEN} 11 | - PUBLIC_BACKEND_HOST=backend-${HOST} 12 | - PUBLIC_BACKEND_PORT=443 13 | - PUBLIC_TURN_HOST=turn-${HOST} 14 | - ORIGIN=https://${HOST} 15 | depends_on: 16 | - backend 17 | - turn 18 | backend: 19 | build: 20 | context: . 21 | dockerfile: Dockerfile 22 | args: 23 | app: backend 24 | environment: 25 | - AUTH_TOKEN=${AUTH_TOKEN} 26 | turn: 27 | image: ghcr.io/processone/eturnal:latest 28 | network_mode: host 29 | hostname: eturnal 30 | container_name: eturnal 31 | restart: unless-stopped 32 | environment: 33 | ETURNAL_SECRET: ${AUTH_TOKEN} 34 | nginx: 35 | image: nginx:1.23.3-alpine 36 | environment: 37 | - NGINX_ENVSUBST_TEMPLATE_SUFFIX=.conf 38 | - SERVER_NAME=${HOST} 39 | volumes: 40 | - ./nginx.conf:/etc/nginx/templates/default.conf.conf:ro 41 | - type: tmpfs 42 | target: /var/lib/coturn 43 | - /etc/letsencrypt:/etc/letsencrypt:ro 44 | ports: 45 | - 80:80 46 | - 443:443 47 | depends_on: 48 | - frontend 49 | - backend 50 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const gameStateSchema = z.object({ 4 | GameState: z.object({ 5 | players: z.array( 6 | z.object({ 7 | id: z.string(), 8 | name: z.string(), 9 | position: z.object({ 10 | x: z.number(), 11 | y: z.number(), 12 | z: z.number(), 13 | }), 14 | rotation: z.object({ 15 | x: z.number(), 16 | y: z.number(), 17 | z: z.number(), 18 | w: z.number(), 19 | }), 20 | volume: z.number(), 21 | }) 22 | ), 23 | audioSettings: z.object({ 24 | maxDistance: z.number().min(0), 25 | refDistance: z.number().min(0), 26 | rolloffFactor: z.number().min(0), 27 | coneInnerAngle: z.number().min(0).max(360), 28 | coneOuterAngle: z.number().min(0).max(360), 29 | coneOuterGain: z.number().min(0).max(1), 30 | staticGainMultiplier: z.number().min(0), 31 | }), 32 | }), 33 | }); 34 | export const defaultGameState = { 35 | players: [], 36 | audioSettings: { 37 | maxDistance: 90, 38 | refDistance: 20, 39 | rolloffFactor: 1, 40 | coneInnerAngle: 360, 41 | coneOuterAngle: 0, 42 | coneOuterGain: 0, 43 | staticGainMultiplier: 1, 44 | }, 45 | } satisfies GameState; 46 | 47 | export type GameState = z.infer['GameState']; 48 | export type Player = GameState['players'][number]; 49 | -------------------------------------------------------------------------------- /apps/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Ottomated" 6 | }, 7 | "scripts": { 8 | "dev": "vite dev", 9 | "build": "vite build", 10 | "start": "node build/index.js", 11 | "lint": "eslint --fix . && svelte-check" 12 | }, 13 | "dependencies": { 14 | "@fontsource/inter": "^4.5.15", 15 | "@tanstack/svelte-query": "^4.26.1", 16 | "common": "workspace:*", 17 | "devalue": "^4.3.0", 18 | "lucide-svelte": "^0.125.0", 19 | "peerjs": "1.4.7", 20 | "tailwindcss": "^3.2.4", 21 | "webrtc-adapter": "^8.2.0" 22 | }, 23 | "devDependencies": { 24 | "@sveltejs/adapter-node": "^1.2.2", 25 | "@sveltejs/kit": "^1.11.0", 26 | "@typescript-eslint/eslint-plugin": "^5.54.1", 27 | "@typescript-eslint/parser": "^5.54.1", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.36.0", 30 | "eslint-config-prettier": "^8.7.0", 31 | "eslint-plugin-prettier": "^4.2.1", 32 | "eslint-plugin-svelte3": "^4.0.0", 33 | "postcss": "^8.4.21", 34 | "postcss-load-config": "^4.0.1", 35 | "prettier": "^2.8.3", 36 | "prettier-plugin-svelte": "^2.9.0", 37 | "svelte": "^3.56.0", 38 | "svelte-check": "^3.1.2", 39 | "svelte-preprocess": "^5.0.1", 40 | "tslib": "^2.5.0", 41 | "typescript": "~4.9.5", 42 | "vite": "^4.1.1" 43 | }, 44 | "type": "module", 45 | "pnpm": { 46 | "patchedDependencies": { 47 | "peerjs@1.4.7": "patches/peerjs@1.4.7.patch" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/setup/+page@.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
10 |

Rules

11 |
    12 |
  • Do not pick Gekko
  • 13 |
  • 14 | Try your best not to stack on top of each other - the person on bottom 15 | will fade out and you won't be able to hear them 16 | Stacking 17 |
  • 18 |
  • Expect jankiness :)
  • 19 |
20 | 21 |

Setup Instructions

22 |
23 |

1. Turn off in-game UI

24 | Hide User Interface In Game 25 |

2. Unbind the scoreboard button

26 | Unbind Scoreboard 27 |

3. Turn off in-game voice chat

28 | Turn Off Voice Chat 29 |

30 | 4. Open the website 35 | on a Chrome-based browser 36 |

37 |
38 |
39 | -------------------------------------------------------------------------------- /apps/backend/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Ottomated (c) 2023 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /apps/frontend/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Ottomated (c) 2023 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /apps/frontend/src/lib/Login.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
34 | {#if $users.isLoading} 35 |

Loading users...

36 | {:else if $users.isError} 37 |

Error: {$users.error}

38 | {:else if $users.data.length === 0} 39 |

Waiting for game to start...

40 | {:else} 41 |

Who are you?

42 | {#each $users.data as user} 43 | 49 | {/each} 50 | {/if} 51 |
52 | -------------------------------------------------------------------------------- /apps/frontend/src/lib/audio.ts: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store'; 2 | import { storedWritable } from './stores'; 3 | 4 | export const SMOOTHING = 0.1; 5 | 6 | export const audioSettings = storedWritable('audioSettings', () => ({ 7 | muted: false, 8 | deafened: false, 9 | inputVolume: 1, 10 | })); 11 | 12 | export async function getMicrophone(context: AudioContext) { 13 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 14 | const node = context.createMediaStreamSource(stream); 15 | return { stream, node }; 16 | } 17 | 18 | export function closeStream(stream: MediaStream | undefined) { 19 | if (!stream) return; 20 | stream.getTracks().forEach((t) => t.stop()); 21 | } 22 | 23 | export function setStreamEnabled( 24 | stream: MediaStream | undefined, 25 | enabled: boolean 26 | ) { 27 | if (!stream) return; 28 | stream.getTracks().forEach((t) => (t.enabled = enabled)); 29 | } 30 | 31 | function canAutoplay() { 32 | const el = document.createElement('audio'); 33 | el.setAttribute('playsinline', 'playsinline'); 34 | el.src = '/_app/immutable/a.mp3'; 35 | return new Promise((resolve) => { 36 | const playResult = el.play(); 37 | const timeout = setTimeout(() => { 38 | cleanup(false); 39 | }, 250); 40 | const cleanup = (res: boolean) => { 41 | resolve(res); 42 | clearTimeout(timeout); 43 | el.remove(); 44 | }; 45 | if (playResult !== undefined) { 46 | playResult.then(() => cleanup(true)).catch(() => cleanup(false)); 47 | } else { 48 | cleanup(true); 49 | } 50 | }); 51 | } 52 | 53 | export const audioAllowed = readable(false, (set) => { 54 | let succeeded = false; 55 | const check = async () => { 56 | const result = await canAutoplay(); 57 | console.log(result); 58 | if (result) succeeded = true; 59 | set(result); 60 | if (succeeded) { 61 | clearInterval(interval); 62 | } 63 | }; 64 | check(); 65 | 66 | const interval = setInterval(check, 1000); 67 | 68 | window.addEventListener('click', check); 69 | }); 70 | -------------------------------------------------------------------------------- /apps/frontend/src/lib/socket.ts: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/public'; 2 | import type { GameState } from 'common'; 3 | import Peer from 'peerjs'; 4 | import { writable } from 'svelte/store'; 5 | import { storedWritable } from './stores'; 6 | 7 | export const myId = storedWritable('playerId', ''); 8 | 9 | export async function createPeer(id: string, token: string) { 10 | const expires = Date.now() + 1000 * 60 * 60 * 24; 11 | 12 | const signature = await crypto.subtle.sign( 13 | { 14 | name: 'HMAC', 15 | hash: 'SHA-1', 16 | }, 17 | await crypto.subtle.importKey( 18 | 'raw', 19 | new TextEncoder().encode(token), 20 | { 21 | name: 'HMAC', 22 | hash: 'SHA-1', 23 | }, 24 | false, 25 | ['sign', 'verify'] 26 | ), 27 | new TextEncoder().encode(expires.toString()) 28 | ); 29 | 30 | const gameState = writable({ 31 | players: [], 32 | falloffDistance: 10, 33 | }); 34 | // base64 35 | const signatureString = btoa( 36 | String.fromCharCode(...new Uint8Array(signature)) 37 | ); 38 | 39 | const peer = new Peer(id, { 40 | host: env.PUBLIC_BACKEND_HOST, 41 | port: Number(env.PUBLIC_BACKEND_PORT), 42 | key: token, 43 | path: '/proximity', 44 | config: { 45 | iceServers: [ 46 | // { urls: 'stun:stun.l.google.com:19302' }, 47 | { 48 | urls: `turn:${env.PUBLIC_TURN_HOST}:3478`, 49 | username: expires.toString(), 50 | credential: signatureString, 51 | }, 52 | ], 53 | }, 54 | }); 55 | 56 | let ws: WebSocket | undefined; 57 | 58 | const onMessage = () => { 59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 60 | ws = (peer.socket as any)._socket as WebSocket | undefined; 61 | if (!ws) return; 62 | peer.socket.off('message', onMessage); 63 | 64 | ws.addEventListener('message', (event) => { 65 | try { 66 | const data = JSON.parse(event.data); 67 | if (data.GameState) { 68 | gameState.set(data.GameState); 69 | } 70 | } catch (e) { 71 | console.error(e); 72 | } 73 | }); 74 | }; 75 | peer.socket.on('message', onMessage); 76 | return [peer, gameState] as const; 77 | } 78 | -------------------------------------------------------------------------------- /apps/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PeerServer } from 'peer'; 2 | import { writable } from './store'; 3 | import { defaultGameState, gameStateSchema } from 'common'; 4 | import type { WebSocket } from 'ws'; 5 | 6 | const authToken = process.env.AUTH_TOKEN ?? 'pass'; 7 | if (!process.env.AUTH_TOKEN) { 8 | console.warn('No AUTH_TOKEN set, using "pass"'); 9 | } 10 | 11 | const peerServer = PeerServer({ 12 | port: 9000, 13 | path: '/proximity', 14 | alive_timeout: Infinity, 15 | key: authToken, 16 | }); 17 | 18 | const state = writable(gameStateSchema, { 19 | GameState: defaultGameState, 20 | }); 21 | 22 | const clients = new Set(); 23 | 24 | state.subscribe((state) => { 25 | clients.forEach((client) => { 26 | client.send(JSON.stringify(state)); 27 | }); 28 | }); 29 | 30 | peerServer.use((_, res, next) => { 31 | res.header('Access-Control-Allow-Origin', '*'); 32 | res.header('Access-Control-Allow-Headers', 'Authorization'); 33 | res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); 34 | next(); 35 | }); 36 | 37 | peerServer.get('/users', (req, res) => { 38 | if (req.header('Authorization') !== authToken) { 39 | res.status(401).send('Unauthorized'); 40 | return; 41 | } 42 | const { GameState } = state.get(); 43 | const players = GameState.players.map((player) => ({ 44 | id: player.id, 45 | name: player.name, 46 | })); 47 | //never cache 48 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 49 | res.header('Content-Type', 'application/json'); 50 | res.send(JSON.stringify(players)); 51 | }); 52 | 53 | peerServer.on('connection', (client) => { 54 | const ws = client.getSocket(); 55 | if (!ws) return; 56 | clients.add(ws); 57 | ws.addEventListener('message', (event) => { 58 | if (typeof event.data !== 'string') return; 59 | try { 60 | const data = JSON.parse(event.data); 61 | if (data.GameState) { 62 | state.set(data); 63 | } 64 | } catch (_) { 65 | /* noop */ 66 | } 67 | }); 68 | ws.send(JSON.stringify(state.get())); 69 | }); 70 | 71 | peerServer.on('disconnect', (client) => { 72 | const ws = client.getSocket(); 73 | if (!ws) return; 74 | clients.delete(ws); 75 | }); 76 | 77 | console.log('Backend listening on 0.0.0.0:9000'); 78 | -------------------------------------------------------------------------------- /apps/frontend/src/routes/(play)/Chat.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
41 | {#await micPromise} 42 |
Waiting for microphone access...
43 | {:then mic} 44 | {#if $audioAllowed} 45 | {@const me = $gameState.players.find((p) => p.id === $myId)} 46 |
    47 | {#each $gameState.players as player (player.id)} 48 | {#if player.id === $myId} 49 | 50 | {:else} 51 | 52 | {/if} 53 | {/each} 54 |
55 | {:else} 56 | 57 | {/if} 58 | {:catch error} 59 |
60 | Microphone Error: {error.message} 61 |
62 | {/await} 63 |
64 | 68 | 72 |
73 | 74 |
75 | -------------------------------------------------------------------------------- /apps/frontend/src/lib/MyPlayer.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 86 | 87 |
  • 88 | 89 | Me 90 |
  • 91 | -------------------------------------------------------------------------------- /apps/example/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 116 | 117 | { 119 | pressedKeys.add(e.key); 120 | justPressedKeys.add(e.key); 121 | }} 122 | on:keyup={(e) => pressedKeys.delete(e.key)} 123 | on:mousemove={(e) => { 124 | mousePos.x = e.clientX; 125 | mousePos.y = e.clientY; 126 | }} 127 | /> 128 | 129 |
    130 | {#each players as player, i} 131 | 137 | 143 | 144 | {/each} 145 |
    146 | -------------------------------------------------------------------------------- /apps/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,yarn,svelte,node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,yarn,svelte,node 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | .pnpm-debug.log* 46 | 47 | # Diagnostic reports (https://nodejs.org/api/report.html) 48 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | 59 | # Coverage directory used by tools like istanbul 60 | coverage 61 | *.lcov 62 | 63 | # nyc test coverage 64 | .nyc_output 65 | 66 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # Bower dependency directory (https://bower.io/) 70 | bower_components 71 | 72 | # node-waf configuration 73 | .lock-wscript 74 | 75 | # Compiled binary addons (https://nodejs.org/api/addons.html) 76 | build/Release 77 | 78 | # Dependency directories 79 | node_modules/ 80 | jspm_packages/ 81 | 82 | # Snowpack dependency directory (https://snowpack.dev/) 83 | web_modules/ 84 | 85 | # TypeScript cache 86 | *.tsbuildinfo 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional stylelint cache 95 | .stylelintcache 96 | 97 | # Microbundle cache 98 | .rpt2_cache/ 99 | .rts2_cache_cjs/ 100 | .rts2_cache_es/ 101 | .rts2_cache_umd/ 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variable files 113 | .env 114 | .env.development.local 115 | .env.test.local 116 | .env.production.local 117 | .env.local 118 | 119 | # parcel-bundler cache (https://parceljs.org/) 120 | .cache 121 | .parcel-cache 122 | 123 | # Next.js build output 124 | .next 125 | out 126 | 127 | # Nuxt.js build / generate output 128 | .nuxt 129 | dist 130 | 131 | # Gatsby files 132 | .cache/ 133 | # Comment in the public line in if your project uses Gatsby and not Next.js 134 | # https://nextjs.org/blog/next-9-1#public-directory-support 135 | # public 136 | 137 | # vuepress build output 138 | .vuepress/dist 139 | 140 | # vuepress v2.x temp and cache directory 141 | .temp 142 | 143 | # Docusaurus cache and generated files 144 | .docusaurus 145 | 146 | # Serverless directories 147 | .serverless/ 148 | 149 | # FuseBox cache 150 | .fusebox/ 151 | 152 | # DynamoDB Local files 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | .vscode-test 160 | 161 | # yarn v2 162 | .yarn/cache 163 | .yarn/unplugged 164 | .yarn/build-state.yml 165 | .yarn/install-state.gz 166 | .pnp.* 167 | 168 | ### Node Patch ### 169 | # Serverless Webpack directories 170 | .webpack/ 171 | 172 | # Optional stylelint cache 173 | 174 | # SvelteKit build / generate output 175 | .svelte-kit 176 | 177 | ### Svelte ### 178 | # gitignore template for the SvelteKit, frontend web component framework 179 | # website: https://kit.svelte.dev/ 180 | 181 | .svelte-kit/ 182 | package 183 | 184 | ### yarn ### 185 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 186 | 187 | .yarn/* 188 | !.yarn/releases 189 | !.yarn/patches 190 | !.yarn/plugins 191 | !.yarn/sdks 192 | !.yarn/versions 193 | 194 | # if you are NOT using Zero-installs, then: 195 | # comment the following lines 196 | # !.yarn/cache 197 | 198 | # and uncomment the following lines 199 | .pnp.* 200 | 201 | # End of https://www.toptal.com/developers/gitignore/api/macos,yarn,svelte,node 202 | -------------------------------------------------------------------------------- /apps/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,yarn,svelte,node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,yarn,svelte,node 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | .pnpm-debug.log* 46 | 47 | # Diagnostic reports (https://nodejs.org/api/report.html) 48 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | 59 | # Coverage directory used by tools like istanbul 60 | coverage 61 | *.lcov 62 | 63 | # nyc test coverage 64 | .nyc_output 65 | 66 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # Bower dependency directory (https://bower.io/) 70 | bower_components 71 | 72 | # node-waf configuration 73 | .lock-wscript 74 | 75 | # Compiled binary addons (https://nodejs.org/api/addons.html) 76 | build/Release 77 | 78 | # Dependency directories 79 | node_modules/ 80 | jspm_packages/ 81 | 82 | # Snowpack dependency directory (https://snowpack.dev/) 83 | web_modules/ 84 | 85 | # TypeScript cache 86 | *.tsbuildinfo 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional stylelint cache 95 | .stylelintcache 96 | 97 | # Microbundle cache 98 | .rpt2_cache/ 99 | .rts2_cache_cjs/ 100 | .rts2_cache_es/ 101 | .rts2_cache_umd/ 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variable files 113 | .env 114 | .env.development.local 115 | .env.test.local 116 | .env.production.local 117 | .env.local 118 | 119 | # parcel-bundler cache (https://parceljs.org/) 120 | .cache 121 | .parcel-cache 122 | 123 | # Next.js build output 124 | .next 125 | out 126 | 127 | # Nuxt.js build / generate output 128 | .nuxt 129 | dist 130 | 131 | # Gatsby files 132 | .cache/ 133 | # Comment in the public line in if your project uses Gatsby and not Next.js 134 | # https://nextjs.org/blog/next-9-1#public-directory-support 135 | # public 136 | 137 | # vuepress build output 138 | .vuepress/dist 139 | 140 | # vuepress v2.x temp and cache directory 141 | .temp 142 | 143 | # Docusaurus cache and generated files 144 | .docusaurus 145 | 146 | # Serverless directories 147 | .serverless/ 148 | 149 | # FuseBox cache 150 | .fusebox/ 151 | 152 | # DynamoDB Local files 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | .vscode-test 160 | 161 | # yarn v2 162 | .yarn/cache 163 | .yarn/unplugged 164 | .yarn/build-state.yml 165 | .yarn/install-state.gz 166 | .pnp.* 167 | 168 | ### Node Patch ### 169 | # Serverless Webpack directories 170 | .webpack/ 171 | 172 | # Optional stylelint cache 173 | 174 | # SvelteKit build / generate output 175 | .svelte-kit 176 | 177 | ### Svelte ### 178 | # gitignore template for the SvelteKit, frontend web component framework 179 | # website: https://kit.svelte.dev/ 180 | 181 | .svelte-kit/ 182 | package 183 | 184 | ### yarn ### 185 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 186 | 187 | .yarn/* 188 | !.yarn/releases 189 | !.yarn/patches 190 | !.yarn/plugins 191 | !.yarn/sdks 192 | !.yarn/versions 193 | 194 | # if you are NOT using Zero-installs, then: 195 | # comment the following lines 196 | # !.yarn/cache 197 | 198 | # and uncomment the following lines 199 | .pnp.* 200 | 201 | # End of https://www.toptal.com/developers/gitignore/api/macos,yarn,svelte,node 202 | -------------------------------------------------------------------------------- /apps/frontend/src/lib/Player.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 186 | 187 |