Spaces:
				
			
			
	
			
			
		Sleeping
		
	
	
	
			
			
	
	
	
	
		
		
		Sleeping
		
	
		github-actions[bot]
		
	commited on
		
		
					Commit 
							
							·
						
						a07d36d
	
0
								Parent(s):
							
							
Sync to HuggingFace Spaces
Browse files- .github/workflows/on-push-to-main.yml +17 -0
 - .gitignore +5 -0
 - .npmrc +1 -0
 - .prettierrc +3 -0
 - Dockerfile +14 -0
 - README.md +59 -0
 - hf-space-config.yml +7 -0
 - license.txt +21 -0
 - package-lock.json +0 -0
 - package-scripts/downloadGameServer.ts +22 -0
 - package-scripts/zip.ts +58 -0
 - package.json +52 -0
 - public/index.html +195 -0
 - public/table.webp +0 -0
 - rollup.config.ts +52 -0
 - screenshot.png +0 -0
 - src/client.ts +433 -0
 - src/server.ts +513 -0
 - src/shared.ts +53 -0
 - src/types.d.ts +26 -0
 - tsconfig.json +11 -0
 
    	
        .github/workflows/on-push-to-main.yml
    ADDED
    
    | 
         @@ -0,0 +1,17 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            name: On Push To Main
         
     | 
| 2 | 
         
            +
            on:
         
     | 
| 3 | 
         
            +
              push:
         
     | 
| 4 | 
         
            +
                branches: ["main"]
         
     | 
| 5 | 
         
            +
            jobs:
         
     | 
| 6 | 
         
            +
              sync-to-hf:
         
     | 
| 7 | 
         
            +
                name: Sync to HuggingFace Spaces
         
     | 
| 8 | 
         
            +
                runs-on: ubuntu-latest
         
     | 
| 9 | 
         
            +
                steps:
         
     | 
| 10 | 
         
            +
                  - uses: actions/checkout@v4
         
     | 
| 11 | 
         
            +
                  - uses: JacobLinCool/huggingface-sync@v1
         
     | 
| 12 | 
         
            +
                    with:
         
     | 
| 13 | 
         
            +
                      github: ${{ secrets.GITHUB_TOKEN }}
         
     | 
| 14 | 
         
            +
                      user: ${{ vars.HF_SPACE_OWNER }}
         
     | 
| 15 | 
         
            +
                      space: ${{ vars.HF_SPACE_NAME }}
         
     | 
| 16 | 
         
            +
                      token: ${{ secrets.HF_TOKEN }}
         
     | 
| 17 | 
         
            +
                      configuration: "hf-space-config.yml"
         
     | 
    	
        .gitignore
    ADDED
    
    | 
         @@ -0,0 +1,5 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            /node_modules
         
     | 
| 2 | 
         
            +
            /js13kserver
         
     | 
| 3 | 
         
            +
            /*.zip
         
     | 
| 4 | 
         
            +
            /.vscode
         
     | 
| 5 | 
         
            +
            .DS_Store
         
     | 
    	
        .npmrc
    ADDED
    
    | 
         @@ -0,0 +1 @@ 
     | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            legacy-peer-deps = true
         
     | 
    	
        .prettierrc
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            {
         
     | 
| 2 | 
         
            +
              "printWidth": 120
         
     | 
| 3 | 
         
            +
            }
         
     | 
    	
        Dockerfile
    ADDED
    
    | 
         @@ -0,0 +1,14 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            FROM node:20-slim
         
     | 
| 2 | 
         
            +
            ENV PORT ${PORT:-7860}
         
     | 
| 3 | 
         
            +
            EXPOSE ${PORT}
         
     | 
| 4 | 
         
            +
            ARG USERNAME=node
         
     | 
| 5 | 
         
            +
            USER ${USERNAME}
         
     | 
| 6 | 
         
            +
            WORKDIR /home/${USERNAME}/app 
         
     | 
| 7 | 
         
            +
            COPY --chown=${USERNAME}:${USERNAME} ./package.json ./package.json
         
     | 
| 8 | 
         
            +
            COPY --chown=${USERNAME}:${USERNAME} ./package-lock.json ./package-lock.json
         
     | 
| 9 | 
         
            +
            COPY --chown=${USERNAME}:${USERNAME} ./.npmrc ./.npmrc
         
     | 
| 10 | 
         
            +
            RUN npm ci
         
     | 
| 11 | 
         
            +
            COPY --chown=${USERNAME}:${USERNAME} . .
         
     | 
| 12 | 
         
            +
            RUN npm run build
         
     | 
| 13 | 
         
            +
            WORKDIR /home/${USERNAME}/app/js13kserver
         
     | 
| 14 | 
         
            +
            CMD [ "index.js" ]
         
     | 
    	
        README.md
    ADDED
    
    | 
         @@ -0,0 +1,59 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            ---
         
     | 
| 2 | 
         
            +
            title: YoYo Haku Pool
         
     | 
| 3 | 
         
            +
            emoji: 🪀🎱
         
     | 
| 4 | 
         
            +
            colorFrom: green
         
     | 
| 5 | 
         
            +
            colorTo: indigo
         
     | 
| 6 | 
         
            +
            short_description: You in control of a yoyo on a multiplayer pool table!
         
     | 
| 7 | 
         
            +
            pinned: false
         
     | 
| 8 | 
         
            +
            sdk: docker
         
     | 
| 9 | 
         
            +
            ---
         
     | 
| 10 | 
         
            +
             
     | 
| 11 | 
         
            +
            # YoYo Haku Pool - A game for JS13K 2022
         
     | 
| 12 | 
         
            +
             
     | 
| 13 | 
         
            +
            YoYo Haku Pool puts you in control of a yoyo on a multiplayer pool table!
         
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
            
         
     | 
| 16 | 
         
            +
             
     | 
| 17 | 
         
            +
            The goal is to keep the highest score as long as possible.
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
            Click or touch the table to pull your yoyo.
         
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
            Each ball has a value, and you should use yoyo maneuvers to bring them into the corner pockets.
         
     | 
| 22 | 
         
            +
             
     | 
| 23 | 
         
            +
            If you push another yoyo into a corner pocket, you get part of their score, implying that you also lose part of your score if you end up in a corner pocket.
         
     | 
| 24 | 
         
            +
             
     | 
| 25 | 
         
            +
            When the table is clean, balls are brought back to the table. Tip: Focus on pocketing the balls with high value first.
         
     | 
| 26 | 
         
            +
             
     | 
| 27 | 
         
            +
            There are several tables in the room, and you can communicate with players from other tables through the chat area.
         
     | 
| 28 | 
         
            +
             
     | 
| 29 | 
         
            +
            You can also run the following commands there:
         
     | 
| 30 | 
         
            +
             
     | 
| 31 | 
         
            +
            > Command: /nick <nickname>  
         
     | 
| 32 | 
         
            +
            > Effect: Changes your nickname.
         
     | 
| 33 | 
         
            +
             
     | 
| 34 | 
         
            +
            > Command: /newtable  
         
     | 
| 35 | 
         
            +
            > Effect: Starts a new game on an empty table.
         
     | 
| 36 | 
         
            +
             
     | 
| 37 | 
         
            +
            > Command: /jointable <number>  
         
     | 
| 38 | 
         
            +
            > Effect: Joins the game from a specific table.
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
            > Command: /soundon  
         
     | 
| 41 | 
         
            +
            > Effect: Enables sounds.
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            > Command: /soundoff  
         
     | 
| 44 | 
         
            +
            > Effect: Disables sounds.
         
     | 
| 45 | 
         
            +
             
     | 
| 46 | 
         
            +
            _The game follows the rules of [JS13K Server Category](https://github.com/js13kGames/js13kserver), which requires us to [host the server on Heroku](https://github.com/js13kGames/js13kserver#deploy-to-heroku)._
         
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
            ## Credits
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
            - Pool Table from the [8 Ball Pool SMS Asset Pack by chasersgaming](https://chasersgaming.itch.io/asset-pack-8-ball-pool-sms)
         
     | 
| 51 | 
         
            +
            - Several NPM Packages, which are listed on [package.json](./package.json)
         
     | 
| 52 | 
         
            +
             
     | 
| 53 | 
         
            +
            ## Tools used during development
         
     | 
| 54 | 
         
            +
             
     | 
| 55 | 
         
            +
            - [Gitpod - Ready-to-code developer environments in the cloud](https://gitpod.io)
         
     | 
| 56 | 
         
            +
            - [Piskel - Free online editor for animated sprites & pixel art](https://www.piskelapp.com)
         
     | 
| 57 | 
         
            +
            - [Squoosh - Reduce file size from a image while maintain high quality](https://squoosh.app)
         
     | 
| 58 | 
         
            +
            - [vConsole - Front-end developer tool for mobile web pages](https://github.com/Tencent/vConsole)
         
     | 
| 59 | 
         
            +
            - [CSS Grid Layout generator](https://vue-grid-generator.netlify.app)
         
     | 
    	
        hf-space-config.yml
    ADDED
    
    | 
         @@ -0,0 +1,7 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            title: YoYo Haku Pool
         
     | 
| 2 | 
         
            +
            emoji: 🪀🎱
         
     | 
| 3 | 
         
            +
            colorFrom: green
         
     | 
| 4 | 
         
            +
            colorTo: indigo
         
     | 
| 5 | 
         
            +
            short_description: You in control of a yoyo on a multiplayer pool table!
         
     | 
| 6 | 
         
            +
            pinned: false
         
     | 
| 7 | 
         
            +
            sdk: docker
         
     | 
    	
        license.txt
    ADDED
    
    | 
         @@ -0,0 +1,21 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            MIT License
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            Copyright (c) 2022 Victor Nogueira
         
     | 
| 4 | 
         
            +
             
     | 
| 5 | 
         
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         
     | 
| 6 | 
         
            +
            of this software and associated documentation files (the "Software"), to deal
         
     | 
| 7 | 
         
            +
            in the Software without restriction, including without limitation the rights
         
     | 
| 8 | 
         
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         
     | 
| 9 | 
         
            +
            copies of the Software, and to permit persons to whom the Software is
         
     | 
| 10 | 
         
            +
            furnished to do so, subject to the following conditions:
         
     | 
| 11 | 
         
            +
             
     | 
| 12 | 
         
            +
            The above copyright notice and this permission notice shall be included in all
         
     | 
| 13 | 
         
            +
            copies or substantial portions of the Software.
         
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         
     | 
| 16 | 
         
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         
     | 
| 17 | 
         
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         
     | 
| 18 | 
         
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         
     | 
| 19 | 
         
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         
     | 
| 20 | 
         
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         
     | 
| 21 | 
         
            +
            SOFTWARE.
         
     | 
    	
        package-lock.json
    ADDED
    
    | 
         The diff for this file is too large to render. 
		See raw diff 
     | 
| 
         | 
    	
        package-scripts/downloadGameServer.ts
    ADDED
    
    | 
         @@ -0,0 +1,22 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import { existsSync } from "node:fs";
         
     | 
| 2 | 
         
            +
            import { resolve } from "node:path";
         
     | 
| 3 | 
         
            +
            import { execSync } from "node:child_process";
         
     | 
| 4 | 
         
            +
            import download from "download";
         
     | 
| 5 | 
         
            +
             
     | 
| 6 | 
         
            +
            const serverFolder = resolve(__dirname, "..", "js13kserver");
         
     | 
| 7 | 
         
            +
             
     | 
| 8 | 
         
            +
            if (existsSync(serverFolder)) process.exit();
         
     | 
| 9 | 
         
            +
             
     | 
| 10 | 
         
            +
            (async () => {
         
     | 
| 11 | 
         
            +
              await download(
         
     | 
| 12 | 
         
            +
                "https://github.com/js13kGames/js13kserver/archive/63a3f1631aaad819d50b5f1b0478f26be3d4700a.zip",
         
     | 
| 13 | 
         
            +
                serverFolder,
         
     | 
| 14 | 
         
            +
                {
         
     | 
| 15 | 
         
            +
                  extract: true,
         
     | 
| 16 | 
         
            +
                  strip: 1,
         
     | 
| 17 | 
         
            +
                }
         
     | 
| 18 | 
         
            +
              );
         
     | 
| 19 | 
         
            +
              console.log("Finished downloading the game server.");
         
     | 
| 20 | 
         
            +
              execSync("npm ci", { cwd: serverFolder });
         
     | 
| 21 | 
         
            +
              console.log("Finished installing game server dependencies.");
         
     | 
| 22 | 
         
            +
            })();
         
     | 
    	
        package-scripts/zip.ts
    ADDED
    
    | 
         @@ -0,0 +1,58 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import { resolve } from "node:path";
         
     | 
| 2 | 
         
            +
            import { createWriteStream, statSync } from "node:fs";
         
     | 
| 3 | 
         
            +
            import { EOL } from "node:os";
         
     | 
| 4 | 
         
            +
            import archiver from "archiver";
         
     | 
| 5 | 
         
            +
            import tasuku from "tasuku";
         
     | 
| 6 | 
         
            +
            import { greenBright, redBright } from "colorette";
         
     | 
| 7 | 
         
            +
            // @ts-ignore
         
     | 
| 8 | 
         
            +
            import crossExecFile from "cross-exec-file";
         
     | 
| 9 | 
         
            +
            // @ts-ignore
         
     | 
| 10 | 
         
            +
            import efficientCompressionTool from "ect-bin";
         
     | 
| 11 | 
         
            +
            // @ts-ignore
         
     | 
| 12 | 
         
            +
            import zipstats from "zipstats";
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            const publicFolderPath = resolve(__dirname, "..", "js13kserver", "public");
         
     | 
| 15 | 
         
            +
            const zipFilePath = resolve(__dirname, "..", "game.zip");
         
     | 
| 16 | 
         
            +
            const archive = archiver("zip", { zlib: { level: 9 } });
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            tasuku.group((task) => [
         
     | 
| 19 | 
         
            +
              task("Creating zip file", async () => {
         
     | 
| 20 | 
         
            +
                return new Promise((resolve, reject) => {
         
     | 
| 21 | 
         
            +
                  const output = createWriteStream(zipFilePath);
         
     | 
| 22 | 
         
            +
                  output.on("close", resolve);
         
     | 
| 23 | 
         
            +
                  output.on("error", reject);
         
     | 
| 24 | 
         
            +
                  archive.pipe(output);
         
     | 
| 25 | 
         
            +
                  archive.directory(publicFolderPath, "");
         
     | 
| 26 | 
         
            +
                  archive.finalize();
         
     | 
| 27 | 
         
            +
                });
         
     | 
| 28 | 
         
            +
              }),
         
     | 
| 29 | 
         
            +
              task("Optimizing zip file", async ({ setOutput, setError }) => {
         
     | 
| 30 | 
         
            +
                const result: { stdout: string; stderr: string } = await crossExecFile(efficientCompressionTool, [
         
     | 
| 31 | 
         
            +
                  "-9",
         
     | 
| 32 | 
         
            +
                  "-zip",
         
     | 
| 33 | 
         
            +
                  zipFilePath,
         
     | 
| 34 | 
         
            +
                ]);
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
                if (result.stderr.length) {
         
     | 
| 37 | 
         
            +
                  setError(result.stderr);
         
     | 
| 38 | 
         
            +
                } else {
         
     | 
| 39 | 
         
            +
                  setOutput(result.stdout);
         
     | 
| 40 | 
         
            +
                }
         
     | 
| 41 | 
         
            +
              }),
         
     | 
| 42 | 
         
            +
              task("Checking zip file", async ({ setOutput }) => {
         
     | 
| 43 | 
         
            +
                setOutput(zipstats(zipFilePath));
         
     | 
| 44 | 
         
            +
              }),
         
     | 
| 45 | 
         
            +
              task("Checking size limit", async ({ setOutput, setError }) => {
         
     | 
| 46 | 
         
            +
                const maxSizeAllowed = 13 * 1024;
         
     | 
| 47 | 
         
            +
                const fileSize = statSync(zipFilePath).size;
         
     | 
| 48 | 
         
            +
                const fileSizeDifference = Math.abs(maxSizeAllowed - fileSize);
         
     | 
| 49 | 
         
            +
                const isUnderSizeLimit = fileSize <= maxSizeAllowed;
         
     | 
| 50 | 
         
            +
                const message = `${fileSizeDifference} bytes ${isUnderSizeLimit ? "under" : "over"} the 13KB limit!${EOL}`;
         
     | 
| 51 | 
         
            +
             
     | 
| 52 | 
         
            +
                if (isUnderSizeLimit) {
         
     | 
| 53 | 
         
            +
                  setOutput(greenBright(message));
         
     | 
| 54 | 
         
            +
                } else {
         
     | 
| 55 | 
         
            +
                  setError(redBright(message));
         
     | 
| 56 | 
         
            +
                }
         
     | 
| 57 | 
         
            +
              }),
         
     | 
| 58 | 
         
            +
            ]);
         
     | 
    	
        package.json
    ADDED
    
    | 
         @@ -0,0 +1,52 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            {
         
     | 
| 2 | 
         
            +
              "name": "js13k-2022",
         
     | 
| 3 | 
         
            +
              "version": "1.0.0",
         
     | 
| 4 | 
         
            +
              "license": "MIT",
         
     | 
| 5 | 
         
            +
              "private": true,
         
     | 
| 6 | 
         
            +
              "scripts": {
         
     | 
| 7 | 
         
            +
                "start": "npm run build && cd js13kserver && node index.js",
         
     | 
| 8 | 
         
            +
                "dev": "npm run build && npm-run-all --parallel --race rollup-watch nodemon",
         
     | 
| 9 | 
         
            +
                "build": "npm-run-all --sequential download-game-server clear-public-folder rollup-build minify-html roadroller zip",
         
     | 
| 10 | 
         
            +
                "nodemon": "cd js13kserver && npx nodemon",
         
     | 
| 11 | 
         
            +
                "rollup-build": "rollup -c",
         
     | 
| 12 | 
         
            +
                "rollup-watch": "rollup -c -w",
         
     | 
| 13 | 
         
            +
                "download-game-server": "ts-node package-scripts/downloadGameServer",
         
     | 
| 14 | 
         
            +
                "zip": "ts-node package-scripts/zip.ts",
         
     | 
| 15 | 
         
            +
                "clear-public-folder": "del js13kserver/public",
         
     | 
| 16 | 
         
            +
                "minify-html": "html-minifier --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --use-short-doctype --minify-css true --minify-js true public/index.html > js13kserver/public/index.html",
         
     | 
| 17 | 
         
            +
                "roadroller": "cd js13kserver/public/ && roadroller client.js -o client.js && roadroller server.js -o server.js"
         
     | 
| 18 | 
         
            +
              },
         
     | 
| 19 | 
         
            +
              "devDependencies": {
         
     | 
| 20 | 
         
            +
                "@rollup/plugin-commonjs": "^22.0.2",
         
     | 
| 21 | 
         
            +
                "@rollup/plugin-node-resolve": "^13.3.0",
         
     | 
| 22 | 
         
            +
                "@rollup/plugin-typescript": "^8.3.4",
         
     | 
| 23 | 
         
            +
                "@types/archiver": "^5.3.1",
         
     | 
| 24 | 
         
            +
                "@types/download": "^8.0.1",
         
     | 
| 25 | 
         
            +
                "@types/mainloop.js": "^1.0.5",
         
     | 
| 26 | 
         
            +
                "archiver": "^5.3.1",
         
     | 
| 27 | 
         
            +
                "colorette": "^2.0.19",
         
     | 
| 28 | 
         
            +
                "create-pubsub": "^1.4.0",
         
     | 
| 29 | 
         
            +
                "cross-exec-file": "^2.0.0",
         
     | 
| 30 | 
         
            +
                "del-cli": "^5.0.0",
         
     | 
| 31 | 
         
            +
                "download": "^8.0.0",
         
     | 
| 32 | 
         
            +
                "ect-bin": "^1.4.1",
         
     | 
| 33 | 
         
            +
                "html-minifier": "^4.0.0",
         
     | 
| 34 | 
         
            +
                "kontra": "^8.0.0",
         
     | 
| 35 | 
         
            +
                "mainloop.js": "^1.0.4",
         
     | 
| 36 | 
         
            +
                "npm-run-all": "^4.1.5",
         
     | 
| 37 | 
         
            +
                "pocket-physics": "^10.1.1",
         
     | 
| 38 | 
         
            +
                "roadroller": "^2.1.0",
         
     | 
| 39 | 
         
            +
                "rollup": "^2.78.0",
         
     | 
| 40 | 
         
            +
                "rollup-plugin-copy": "^3.4.0",
         
     | 
| 41 | 
         
            +
                "rollup-plugin-terser": "^7.0.2",
         
     | 
| 42 | 
         
            +
                "rollup-plugin-watch": "^1.0.1",
         
     | 
| 43 | 
         
            +
                "socket.io": "^4.5.1",
         
     | 
| 44 | 
         
            +
                "socket.io-client": "^4.5.1",
         
     | 
| 45 | 
         
            +
                "tasuku": "^2.0.0",
         
     | 
| 46 | 
         
            +
                "ts-node": "^10.9.1",
         
     | 
| 47 | 
         
            +
                "tslib": "^2.4.0",
         
     | 
| 48 | 
         
            +
                "typescript": "^4.7.4",
         
     | 
| 49 | 
         
            +
                "zipstats": "^1.1.0",
         
     | 
| 50 | 
         
            +
                "zzfx": "^1.1.8"
         
     | 
| 51 | 
         
            +
              }
         
     | 
| 52 | 
         
            +
            }
         
     | 
    	
        public/index.html
    ADDED
    
    | 
         @@ -0,0 +1,195 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            <!DOCTYPE html>
         
     | 
| 2 | 
         
            +
            <html lang="en">
         
     | 
| 3 | 
         
            +
              <head>
         
     | 
| 4 | 
         
            +
                <meta charset="UTF-8" />
         
     | 
| 5 | 
         
            +
                <link
         
     | 
| 6 | 
         
            +
                  rel="icon"
         
     | 
| 7 | 
         
            +
                  href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎱</text></svg>"
         
     | 
| 8 | 
         
            +
                />
         
     | 
| 9 | 
         
            +
                <meta name="viewport" content="width=device-width, user-scalable=no" />
         
     | 
| 10 | 
         
            +
                <meta name="monetization" content="$ilp.uphold.com/JKriL7R2DZfa" />
         
     | 
| 11 | 
         
            +
                <style>
         
     | 
| 12 | 
         
            +
                  :root {
         
     | 
| 13 | 
         
            +
                    --inner-height: 100vh;
         
     | 
| 14 | 
         
            +
                  }
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
                  * {
         
     | 
| 17 | 
         
            +
                    margin: 0;
         
     | 
| 18 | 
         
            +
                    padding: 0;
         
     | 
| 19 | 
         
            +
                    outline: none;
         
     | 
| 20 | 
         
            +
                  }
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
                  html,
         
     | 
| 23 | 
         
            +
                  body {
         
     | 
| 24 | 
         
            +
                    width: 100vw;
         
     | 
| 25 | 
         
            +
                    height: var(--inner-height);
         
     | 
| 26 | 
         
            +
                    overflow: hidden;
         
     | 
| 27 | 
         
            +
                    background-color: #370000;
         
     | 
| 28 | 
         
            +
                    touch-action: none;
         
     | 
| 29 | 
         
            +
                  }
         
     | 
| 30 | 
         
            +
             
     | 
| 31 | 
         
            +
                  img {
         
     | 
| 32 | 
         
            +
                    display: none;
         
     | 
| 33 | 
         
            +
                  }
         
     | 
| 34 | 
         
            +
             
     | 
| 35 | 
         
            +
                  button {
         
     | 
| 36 | 
         
            +
                    cursor: pointer;
         
     | 
| 37 | 
         
            +
                  }
         
     | 
| 38 | 
         
            +
             
     | 
| 39 | 
         
            +
                  #a {
         
     | 
| 40 | 
         
            +
                    display: grid;
         
     | 
| 41 | 
         
            +
                    width: 100%;
         
     | 
| 42 | 
         
            +
                    height: 100%;
         
     | 
| 43 | 
         
            +
                  }
         
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
                  @media (orientation: landscape) {
         
     | 
| 46 | 
         
            +
                    #a {
         
     | 
| 47 | 
         
            +
                      grid-template-areas: "a1 a2";
         
     | 
| 48 | 
         
            +
                      grid-template-columns: 300px 1fr;
         
     | 
| 49 | 
         
            +
                    }
         
     | 
| 50 | 
         
            +
             
     | 
| 51 | 
         
            +
                    #canvas {
         
     | 
| 52 | 
         
            +
                      max-width: calc(100vw - 300px);
         
     | 
| 53 | 
         
            +
                      max-height: var(--inner-height);
         
     | 
| 54 | 
         
            +
                    }
         
     | 
| 55 | 
         
            +
                  }
         
     | 
| 56 | 
         
            +
             
     | 
| 57 | 
         
            +
                  @media (orientation: portrait) {
         
     | 
| 58 | 
         
            +
                    #a {
         
     | 
| 59 | 
         
            +
                      grid-template-areas:
         
     | 
| 60 | 
         
            +
                        "a1"
         
     | 
| 61 | 
         
            +
                        "a2";
         
     | 
| 62 | 
         
            +
                      grid-template-rows: 200px 1fr;
         
     | 
| 63 | 
         
            +
                    }
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
                    #canvas {
         
     | 
| 66 | 
         
            +
                      max-width: 100vw;
         
     | 
| 67 | 
         
            +
                      max-height: calc(var(--inner-height) - 200px);
         
     | 
| 68 | 
         
            +
                    }
         
     | 
| 69 | 
         
            +
                  }
         
     | 
| 70 | 
         
            +
             
     | 
| 71 | 
         
            +
                  #a1 {
         
     | 
| 72 | 
         
            +
                    grid-area: a1;
         
     | 
| 73 | 
         
            +
                  }
         
     | 
| 74 | 
         
            +
             
     | 
| 75 | 
         
            +
                  #a2 {
         
     | 
| 76 | 
         
            +
                    grid-area: a2;
         
     | 
| 77 | 
         
            +
                  }
         
     | 
| 78 | 
         
            +
             
     | 
| 79 | 
         
            +
                  #b {
         
     | 
| 80 | 
         
            +
                    display: grid;
         
     | 
| 81 | 
         
            +
                    width: 100%;
         
     | 
| 82 | 
         
            +
                    height: 100%;
         
     | 
| 83 | 
         
            +
                    max-height: var(--inner-height);
         
     | 
| 84 | 
         
            +
                  }
         
     | 
| 85 | 
         
            +
             
     | 
| 86 | 
         
            +
                  @media (orientation: landscape) {
         
     | 
| 87 | 
         
            +
                    #b {
         
     | 
| 88 | 
         
            +
                      grid-template-areas:
         
     | 
| 89 | 
         
            +
                        "b1 b1"
         
     | 
| 90 | 
         
            +
                        "b2 b2"
         
     | 
| 91 | 
         
            +
                        "b3 b4";
         
     | 
| 92 | 
         
            +
                      grid-template-columns: 1fr 56px;
         
     | 
| 93 | 
         
            +
                      grid-template-rows: 1fr 2fr 36px;
         
     | 
| 94 | 
         
            +
                    }
         
     | 
| 95 | 
         
            +
                  }
         
     | 
| 96 | 
         
            +
             
     | 
| 97 | 
         
            +
                  @media (orientation: portrait) {
         
     | 
| 98 | 
         
            +
                    #b {
         
     | 
| 99 | 
         
            +
                      grid-template-areas:
         
     | 
| 100 | 
         
            +
                        "b2 b2 b1"
         
     | 
| 101 | 
         
            +
                        "b3 b4 b1";
         
     | 
| 102 | 
         
            +
                      grid-template-columns: 1fr 56px 1fr;
         
     | 
| 103 | 
         
            +
                      grid-template-rows: 1fr 36px;
         
     | 
| 104 | 
         
            +
                    }
         
     | 
| 105 | 
         
            +
                  }
         
     | 
| 106 | 
         
            +
             
     | 
| 107 | 
         
            +
                  #b1 {
         
     | 
| 108 | 
         
            +
                    grid-area: b1;
         
     | 
| 109 | 
         
            +
                  }
         
     | 
| 110 | 
         
            +
             
     | 
| 111 | 
         
            +
                  #b2 {
         
     | 
| 112 | 
         
            +
                    grid-area: b2;
         
     | 
| 113 | 
         
            +
                  }
         
     | 
| 114 | 
         
            +
             
     | 
| 115 | 
         
            +
                  #b1,
         
     | 
| 116 | 
         
            +
                  #b2 {
         
     | 
| 117 | 
         
            +
                    background-color: #550000;
         
     | 
| 118 | 
         
            +
                    padding: 5px;
         
     | 
| 119 | 
         
            +
                    border: 3px solid #aa0000;
         
     | 
| 120 | 
         
            +
                  }
         
     | 
| 121 | 
         
            +
             
     | 
| 122 | 
         
            +
                  #b3 {
         
     | 
| 123 | 
         
            +
                    grid-area: b3;
         
     | 
| 124 | 
         
            +
                    background-color: #550000;
         
     | 
| 125 | 
         
            +
                    padding: 0 5px;
         
     | 
| 126 | 
         
            +
                    border: 3px solid #aa0000;
         
     | 
| 127 | 
         
            +
                  }
         
     | 
| 128 | 
         
            +
             
     | 
| 129 | 
         
            +
                  #b4 {
         
     | 
| 130 | 
         
            +
                    grid-area: b4;
         
     | 
| 131 | 
         
            +
                    border: 3px solid #aa0000;
         
     | 
| 132 | 
         
            +
                  }
         
     | 
| 133 | 
         
            +
             
     | 
| 134 | 
         
            +
                  #b textarea,
         
     | 
| 135 | 
         
            +
                  #b input,
         
     | 
| 136 | 
         
            +
                  #b button {
         
     | 
| 137 | 
         
            +
                    width: 100%;
         
     | 
| 138 | 
         
            +
                    height: 100%;
         
     | 
| 139 | 
         
            +
                    border: none;
         
     | 
| 140 | 
         
            +
                    color: white;
         
     | 
| 141 | 
         
            +
                    font-family: "Courier New", Courier, monospace;
         
     | 
| 142 | 
         
            +
                    font-weight: bold;
         
     | 
| 143 | 
         
            +
                  }
         
     | 
| 144 | 
         
            +
             
     | 
| 145 | 
         
            +
                  #b input {
         
     | 
| 146 | 
         
            +
                    background-color: #550000;
         
     | 
| 147 | 
         
            +
                  }
         
     | 
| 148 | 
         
            +
             
     | 
| 149 | 
         
            +
                  #b textarea {
         
     | 
| 150 | 
         
            +
                    background-color: #550000;
         
     | 
| 151 | 
         
            +
                    resize: none;
         
     | 
| 152 | 
         
            +
                  }
         
     | 
| 153 | 
         
            +
             
     | 
| 154 | 
         
            +
                  #b textarea:disabled {
         
     | 
| 155 | 
         
            +
                    opacity: 1;
         
     | 
| 156 | 
         
            +
                  }
         
     | 
| 157 | 
         
            +
             
     | 
| 158 | 
         
            +
                  #b button {
         
     | 
| 159 | 
         
            +
                    background-color: #55aa00;
         
     | 
| 160 | 
         
            +
                    text-align: center;
         
     | 
| 161 | 
         
            +
                    display: inline-block;
         
     | 
| 162 | 
         
            +
                  }
         
     | 
| 163 | 
         
            +
             
     | 
| 164 | 
         
            +
                  #b1 textarea {
         
     | 
| 165 | 
         
            +
                    white-space: pre;
         
     | 
| 166 | 
         
            +
                  }
         
     | 
| 167 | 
         
            +
                </style>
         
     | 
| 168 | 
         
            +
              </head>
         
     | 
| 169 | 
         
            +
              <body>
         
     | 
| 170 | 
         
            +
                <div id="a">
         
     | 
| 171 | 
         
            +
                  <div id="a1">
         
     | 
| 172 | 
         
            +
                    <div id="b">
         
     | 
| 173 | 
         
            +
                      <div id="b1">
         
     | 
| 174 | 
         
            +
                        <textarea disabled="disabled" wrap="off">CONNECTING...</textarea>
         
     | 
| 175 | 
         
            +
                      </div>
         
     | 
| 176 | 
         
            +
                      <div id="b2">
         
     | 
| 177 | 
         
            +
                        <textarea disabled="disabled"></textarea>
         
     | 
| 178 | 
         
            +
                      </div>
         
     | 
| 179 | 
         
            +
                      <div id="b3">
         
     | 
| 180 | 
         
            +
                        <input placeholder="Message" type="text" autocomplete="off" autocapitalize="off" />
         
     | 
| 181 | 
         
            +
                      </div>
         
     | 
| 182 | 
         
            +
                      <div id="b4">
         
     | 
| 183 | 
         
            +
                        <button>Send</button>
         
     | 
| 184 | 
         
            +
                      </div>
         
     | 
| 185 | 
         
            +
                    </div>
         
     | 
| 186 | 
         
            +
                  </div>
         
     | 
| 187 | 
         
            +
                  <div id="a2">
         
     | 
| 188 | 
         
            +
                    <canvas id="canvas"></canvas>
         
     | 
| 189 | 
         
            +
                  </div>
         
     | 
| 190 | 
         
            +
                </div>
         
     | 
| 191 | 
         
            +
                <img src="table.webp" />
         
     | 
| 192 | 
         
            +
                <script src="socket.io/socket.io.js"></script>
         
     | 
| 193 | 
         
            +
                <script src="client.js"></script>
         
     | 
| 194 | 
         
            +
              </body>
         
     | 
| 195 | 
         
            +
            </html>
         
     | 
    	
        public/table.webp
    ADDED
    
    
											 
									 | 
									
								
    	
        rollup.config.ts
    ADDED
    
    | 
         @@ -0,0 +1,52 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import { defineConfig } from "rollup";
         
     | 
| 2 | 
         
            +
            import { terser } from "rollup-plugin-terser";
         
     | 
| 3 | 
         
            +
            import { nodeResolve } from "@rollup/plugin-node-resolve";
         
     | 
| 4 | 
         
            +
            import typescript from "@rollup/plugin-typescript";
         
     | 
| 5 | 
         
            +
            import commonjs from "@rollup/plugin-commonjs";
         
     | 
| 6 | 
         
            +
            import copy from "rollup-plugin-copy";
         
     | 
| 7 | 
         
            +
            import watch from "rollup-plugin-watch";
         
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            export default defineConfig([
         
     | 
| 10 | 
         
            +
              {
         
     | 
| 11 | 
         
            +
                input: "src/server.ts",
         
     | 
| 12 | 
         
            +
                output: [
         
     | 
| 13 | 
         
            +
                  {
         
     | 
| 14 | 
         
            +
                    file: "js13kserver/public/server.js",
         
     | 
| 15 | 
         
            +
                    format: "commonjs",
         
     | 
| 16 | 
         
            +
                    exports: "default",
         
     | 
| 17 | 
         
            +
                  },
         
     | 
| 18 | 
         
            +
                ],
         
     | 
| 19 | 
         
            +
                plugins: [
         
     | 
| 20 | 
         
            +
                  typescript(),
         
     | 
| 21 | 
         
            +
                  nodeResolve(),
         
     | 
| 22 | 
         
            +
                  commonjs(),
         
     | 
| 23 | 
         
            +
                  terser({
         
     | 
| 24 | 
         
            +
                    format: {
         
     | 
| 25 | 
         
            +
                      comments: false,
         
     | 
| 26 | 
         
            +
                    },
         
     | 
| 27 | 
         
            +
                  }),
         
     | 
| 28 | 
         
            +
                ],
         
     | 
| 29 | 
         
            +
              },
         
     | 
| 30 | 
         
            +
              {
         
     | 
| 31 | 
         
            +
                input: "src/client.ts",
         
     | 
| 32 | 
         
            +
                output: [
         
     | 
| 33 | 
         
            +
                  {
         
     | 
| 34 | 
         
            +
                    file: "js13kserver/public/client.js",
         
     | 
| 35 | 
         
            +
                    format: "iife",
         
     | 
| 36 | 
         
            +
                    name: "client",
         
     | 
| 37 | 
         
            +
                  },
         
     | 
| 38 | 
         
            +
                ],
         
     | 
| 39 | 
         
            +
                plugins: [
         
     | 
| 40 | 
         
            +
                  typescript(),
         
     | 
| 41 | 
         
            +
                  nodeResolve(),
         
     | 
| 42 | 
         
            +
                  commonjs(),
         
     | 
| 43 | 
         
            +
                  terser({
         
     | 
| 44 | 
         
            +
                    format: {
         
     | 
| 45 | 
         
            +
                      comments: false,
         
     | 
| 46 | 
         
            +
                    },
         
     | 
| 47 | 
         
            +
                  }),
         
     | 
| 48 | 
         
            +
                  watch({ dir: "public" }),
         
     | 
| 49 | 
         
            +
                  copy({ targets: [{ src: "public/**/*", dest: "js13kserver/public" }] }),
         
     | 
| 50 | 
         
            +
                ],
         
     | 
| 51 | 
         
            +
              },
         
     | 
| 52 | 
         
            +
            ]);
         
     | 
    	
        screenshot.png
    ADDED
    
    
											 
									 | 
									
								
    	
        src/client.ts
    ADDED
    
    | 
         @@ -0,0 +1,433 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import type { Socket } from "socket.io-client";
         
     | 
| 2 | 
         
            +
            import { createPubSub } from "create-pubsub";
         
     | 
| 3 | 
         
            +
            import {
         
     | 
| 4 | 
         
            +
              init,
         
     | 
| 5 | 
         
            +
              GameLoop,
         
     | 
| 6 | 
         
            +
              Vector,
         
     | 
| 7 | 
         
            +
              Text,
         
     | 
| 8 | 
         
            +
              Sprite,
         
     | 
| 9 | 
         
            +
              initPointer,
         
     | 
| 10 | 
         
            +
              onPointer,
         
     | 
| 11 | 
         
            +
              getPointer,
         
     | 
| 12 | 
         
            +
              degToRad,
         
     | 
| 13 | 
         
            +
              radToDeg,
         
     | 
| 14 | 
         
            +
              Pool,
         
     | 
| 15 | 
         
            +
            } from "kontra";
         
     | 
| 16 | 
         
            +
            import {
         
     | 
| 17 | 
         
            +
              BallsPositions,
         
     | 
| 18 | 
         
            +
              ballsPositionsUpdatesPerSecond,
         
     | 
| 19 | 
         
            +
              Ball,
         
     | 
| 20 | 
         
            +
              squareCanvasSizeInPixels,
         
     | 
| 21 | 
         
            +
              ServerToClientEvents,
         
     | 
| 22 | 
         
            +
              ClientToServerEvents,
         
     | 
| 23 | 
         
            +
              ballRadius,
         
     | 
| 24 | 
         
            +
              ClientToServerEventName,
         
     | 
| 25 | 
         
            +
              ServerToClientEventName,
         
     | 
| 26 | 
         
            +
              Scoreboard,
         
     | 
| 27 | 
         
            +
            } from "./shared";
         
     | 
| 28 | 
         
            +
            import { zzfx } from "zzfx";
         
     | 
| 29 | 
         
            +
             
     | 
| 30 | 
         
            +
            const gameName = "YoYo Haku Pool";
         
     | 
| 31 | 
         
            +
             
     | 
| 32 | 
         
            +
            const gameFramesPerSecond = 60;
         
     | 
| 33 | 
         
            +
             
     | 
| 34 | 
         
            +
            const gameStateUpdateFramesInterval = gameFramesPerSecond / ballsPositionsUpdatesPerSecond;
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
            const ballIdToBallSpriteMap = new Map<number, Sprite>();
         
     | 
| 37 | 
         
            +
             
     | 
| 38 | 
         
            +
            const { canvas, context } = init(document.querySelector("#canvas") as HTMLCanvasElement);
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
            const scoreboardTextArea = document.querySelector("#b1 textarea") as HTMLTextAreaElement;
         
     | 
| 41 | 
         
            +
             
     | 
| 42 | 
         
            +
            const chatHistory = document.querySelector("#b2 textarea") as HTMLTextAreaElement;
         
     | 
| 43 | 
         
            +
             
     | 
| 44 | 
         
            +
            const chatInputField = document.querySelector("#b3 input") as HTMLInputElement;
         
     | 
| 45 | 
         
            +
             
     | 
| 46 | 
         
            +
            const chatButton = document.querySelector("#b4 button") as HTMLButtonElement;
         
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
            const tableImage = document.querySelector("img[src='table.webp']") as HTMLImageElement;
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
            const socket = io({ upgrade: false, transports: ["websocket"] }) as Socket<ServerToClientEvents, ClientToServerEvents>;
         
     | 
| 51 | 
         
            +
             
     | 
| 52 | 
         
            +
            const [publishMainLoopUpdate, subscribeToMainLoopUpdate] = createPubSub<number>();
         
     | 
| 53 | 
         
            +
             
     | 
| 54 | 
         
            +
            const [publishMainLoopDraw, subscribeToMainLoopDraw] = createPubSub<number>();
         
     | 
| 55 | 
         
            +
             
     | 
| 56 | 
         
            +
            const [publishPageWithImagesLoaded, subscribeToPageWithImagesLoaded] = createPubSub<Event>();
         
     | 
| 57 | 
         
            +
             
     | 
| 58 | 
         
            +
            const [publishGamePreparationComplete, subscribeToGamePreparationCompleted] = createPubSub();
         
     | 
| 59 | 
         
            +
             
     | 
| 60 | 
         
            +
            const [publishPointerPressed, subscribeToPointerPressed, isPointerPressed] = createPubSub(false);
         
     | 
| 61 | 
         
            +
             
     | 
| 62 | 
         
            +
            const [setOwnSprite, , getOwnSprite] = createPubSub<Sprite | null>(null);
         
     | 
| 63 | 
         
            +
             
     | 
| 64 | 
         
            +
            const [setLastTimeEmittedPointerPressed, , getLastTimeEmittedPointerPressed] = createPubSub(Date.now());
         
     | 
| 65 | 
         
            +
             
     | 
| 66 | 
         
            +
            const [publishSoundEnabled, , isSoundEnabled] = createPubSub(false);
         
     | 
| 67 | 
         
            +
             
     | 
| 68 | 
         
            +
            const messageReceivedSound = [2.01, , 773, 0.02, 0.01, 0.01, 1, 1.14, 44, -27, , , , , 0.9, , 0.18, 0.81, 0.01];
         
     | 
| 69 | 
         
            +
             
     | 
| 70 | 
         
            +
            const scoreIncreasedSound = [
         
     | 
| 71 | 
         
            +
              1.35,
         
     | 
| 72 | 
         
            +
              ,
         
     | 
| 73 | 
         
            +
              151,
         
     | 
| 74 | 
         
            +
              0.1,
         
     | 
| 75 | 
         
            +
              0.17,
         
     | 
| 76 | 
         
            +
              0.26,
         
     | 
| 77 | 
         
            +
              1,
         
     | 
| 78 | 
         
            +
              0.34,
         
     | 
| 79 | 
         
            +
              -4.1,
         
     | 
| 80 | 
         
            +
              -5,
         
     | 
| 81 | 
         
            +
              -225,
         
     | 
| 82 | 
         
            +
              0.02,
         
     | 
| 83 | 
         
            +
              0.14,
         
     | 
| 84 | 
         
            +
              0.1,
         
     | 
| 85 | 
         
            +
              ,
         
     | 
| 86 | 
         
            +
              0.1,
         
     | 
| 87 | 
         
            +
              0.13,
         
     | 
| 88 | 
         
            +
              0.9,
         
     | 
| 89 | 
         
            +
              0.22,
         
     | 
| 90 | 
         
            +
              0.17,
         
     | 
| 91 | 
         
            +
            ];
         
     | 
| 92 | 
         
            +
             
     | 
| 93 | 
         
            +
            const acceleratingSound = [, , 999, 0.2, 0.04, 0.15, 4, 2.66, -0.5, 22, , , , 0.1, , , , , 0.02];
         
     | 
| 94 | 
         
            +
             
     | 
| 95 | 
         
            +
            const scoreDecreasedSound = [, , 727, 0.02, 0.03, 0, 3, 0.09, 4.4, -62, , , , , , , 0.19, 0.65, 0.2, 0.51];
         
     | 
| 96 | 
         
            +
             
     | 
| 97 | 
         
            +
            const tableSprite = Sprite({
         
     | 
| 98 | 
         
            +
              image: tableImage,
         
     | 
| 99 | 
         
            +
            });
         
     | 
| 100 | 
         
            +
             
     | 
| 101 | 
         
            +
            const scoreTextPool = Pool({
         
     | 
| 102 | 
         
            +
              create: Text as any,
         
     | 
| 103 | 
         
            +
            });
         
     | 
| 104 | 
         
            +
             
     | 
| 105 | 
         
            +
            function setCanvasWidthAndHeight() {
         
     | 
| 106 | 
         
            +
              canvas.width = canvas.height = squareCanvasSizeInPixels;
         
     | 
| 107 | 
         
            +
            }
         
     | 
| 108 | 
         
            +
             
     | 
| 109 | 
         
            +
            function prepareGame() {
         
     | 
| 110 | 
         
            +
              updateDocumentTitleWithGameName();
         
     | 
| 111 | 
         
            +
              printWelcomeMessage();
         
     | 
| 112 | 
         
            +
              setCanvasWidthAndHeight();
         
     | 
| 113 | 
         
            +
              handleWindowResized();
         
     | 
| 114 | 
         
            +
              initPointer({ radius: 0 });
         
     | 
| 115 | 
         
            +
              publishGamePreparationComplete();
         
     | 
| 116 | 
         
            +
            }
         
     | 
| 117 | 
         
            +
             
     | 
| 118 | 
         
            +
            function emitPointerPressedIfNeeded() {
         
     | 
| 119 | 
         
            +
              if (!isPointerPressed() || Date.now() - getLastTimeEmittedPointerPressed() < 1000 / ballsPositionsUpdatesPerSecond)
         
     | 
| 120 | 
         
            +
                return;
         
     | 
| 121 | 
         
            +
              const { x, y } = getPointer();
         
     | 
| 122 | 
         
            +
              socket.emit(ClientToServerEventName.Click, Math.trunc(x), Math.trunc(y));
         
     | 
| 123 | 
         
            +
              setLastTimeEmittedPointerPressed(Date.now());
         
     | 
| 124 | 
         
            +
            }
         
     | 
| 125 | 
         
            +
             
     | 
| 126 | 
         
            +
            function updateScene() {
         
     | 
| 127 | 
         
            +
              emitPointerPressedIfNeeded();
         
     | 
| 128 | 
         
            +
             
     | 
| 129 | 
         
            +
              ballIdToBallSpriteMap.forEach((sprite) => {
         
     | 
| 130 | 
         
            +
                const newRotationDegree = radToDeg(sprite.rotation) + (Math.abs(sprite.dx) + Math.abs(sprite.dy)) * 7;
         
     | 
| 131 | 
         
            +
                sprite.rotation = degToRad(newRotationDegree > 360 ? newRotationDegree - 360 : newRotationDegree);
         
     | 
| 132 | 
         
            +
                sprite.update();
         
     | 
| 133 | 
         
            +
              });
         
     | 
| 134 | 
         
            +
             
     | 
| 135 | 
         
            +
              scoreTextPool.update();
         
     | 
| 136 | 
         
            +
            }
         
     | 
| 137 | 
         
            +
             
     | 
| 138 | 
         
            +
            function drawLine(fromPoint: { x: number; y: number }, toPoint: { x: number; y: number }) {
         
     | 
| 139 | 
         
            +
              context.beginPath();
         
     | 
| 140 | 
         
            +
              context.strokeStyle = "#fff";
         
     | 
| 141 | 
         
            +
              context.moveTo(fromPoint.x, fromPoint.y);
         
     | 
| 142 | 
         
            +
              context.lineTo(toPoint.x, toPoint.y);
         
     | 
| 143 | 
         
            +
              context.stroke();
         
     | 
| 144 | 
         
            +
            }
         
     | 
| 145 | 
         
            +
             
     | 
| 146 | 
         
            +
            function renderOtherSprites() {
         
     | 
| 147 | 
         
            +
              ballIdToBallSpriteMap.forEach((sprite) => {
         
     | 
| 148 | 
         
            +
                if (sprite !== getOwnSprite()) sprite.render();
         
     | 
| 149 | 
         
            +
              });
         
     | 
| 150 | 
         
            +
            }
         
     | 
| 151 | 
         
            +
             
     | 
| 152 | 
         
            +
            function renderOwnSpritePossiblyWithWire() {
         
     | 
| 153 | 
         
            +
              const ownSprite = getOwnSprite();
         
     | 
| 154 | 
         
            +
             
     | 
| 155 | 
         
            +
              if (!ownSprite) return;
         
     | 
| 156 | 
         
            +
             
     | 
| 157 | 
         
            +
              if (isPointerPressed()) drawLine(ownSprite.position, getPointer());
         
     | 
| 158 | 
         
            +
             
     | 
| 159 | 
         
            +
              ownSprite.render();
         
     | 
| 160 | 
         
            +
            }
         
     | 
| 161 | 
         
            +
             
     | 
| 162 | 
         
            +
            function renderScene() {
         
     | 
| 163 | 
         
            +
              tableSprite.render();
         
     | 
| 164 | 
         
            +
              renderOtherSprites();
         
     | 
| 165 | 
         
            +
              renderOwnSpritePossiblyWithWire();
         
     | 
| 166 | 
         
            +
              scoreTextPool.render();
         
     | 
| 167 | 
         
            +
            }
         
     | 
| 168 | 
         
            +
             
     | 
| 169 | 
         
            +
            function startMainLoop() {
         
     | 
| 170 | 
         
            +
              return GameLoop({ update: publishMainLoopUpdate, render: publishMainLoopDraw }).start();
         
     | 
| 171 | 
         
            +
            }
         
     | 
| 172 | 
         
            +
             
     | 
| 173 | 
         
            +
            function fitCanvasInsideItsParent(canvasElement: HTMLCanvasElement) {
         
     | 
| 174 | 
         
            +
              if (!canvasElement.parentElement) return;
         
     | 
| 175 | 
         
            +
              const { width, height, style, parentElement } = canvasElement;
         
     | 
| 176 | 
         
            +
              const { clientWidth, clientHeight } = parentElement;
         
     | 
| 177 | 
         
            +
              const widthScale = clientWidth / width;
         
     | 
| 178 | 
         
            +
              const heightScale = clientHeight / height;
         
     | 
| 179 | 
         
            +
              const scale = widthScale < heightScale ? widthScale : heightScale;
         
     | 
| 180 | 
         
            +
              style.marginTop = `${(clientHeight - height * scale) / 2}px`;
         
     | 
| 181 | 
         
            +
              style.marginLeft = `${(clientWidth - width * scale) / 2}px`;
         
     | 
| 182 | 
         
            +
              style.width = `${width * scale}px`;
         
     | 
| 183 | 
         
            +
              style.height = `${height * scale}px`;
         
     | 
| 184 | 
         
            +
            }
         
     | 
| 185 | 
         
            +
             
     | 
| 186 | 
         
            +
            function handleBallsPositionsReceived(balls: Ball[]) {
         
     | 
| 187 | 
         
            +
              ballIdToBallSpriteMap.clear();
         
     | 
| 188 | 
         
            +
             
     | 
| 189 | 
         
            +
              balls.forEach((ball) => createSpriteForBall(ball));
         
     | 
| 190 | 
         
            +
            }
         
     | 
| 191 | 
         
            +
             
     | 
| 192 | 
         
            +
            function createSpriteForBall(ball: Ball) {
         
     | 
| 193 | 
         
            +
              const sprite = Sprite({
         
     | 
| 194 | 
         
            +
                x: squareCanvasSizeInPixels / 2,
         
     | 
| 195 | 
         
            +
                y: squareCanvasSizeInPixels / 2,
         
     | 
| 196 | 
         
            +
                anchor: { x: 0.5, y: 0.5 },
         
     | 
| 197 | 
         
            +
                render: () => {
         
     | 
| 198 | 
         
            +
                  sprite.context.fillStyle = ball.color;
         
     | 
| 199 | 
         
            +
                  sprite.context.beginPath();
         
     | 
| 200 | 
         
            +
                  sprite.context.arc(0, 0, ballRadius, 0, 2 * Math.PI);
         
     | 
| 201 | 
         
            +
                  sprite.context.fill();
         
     | 
| 202 | 
         
            +
                },
         
     | 
| 203 | 
         
            +
              });
         
     | 
| 204 | 
         
            +
             
     | 
| 205 | 
         
            +
              const whiteCircle = Sprite({
         
     | 
| 206 | 
         
            +
                anchor: { x: 0.5, y: 0.5 },
         
     | 
| 207 | 
         
            +
                render: () => {
         
     | 
| 208 | 
         
            +
                  sprite.context.fillStyle = "#fff";
         
     | 
| 209 | 
         
            +
                  sprite.context.beginPath();
         
     | 
| 210 | 
         
            +
                  sprite.context.arc(0, 0, ballRadius / 1.5, 0, 2 * Math.PI);
         
     | 
| 211 | 
         
            +
                  sprite.context.fill();
         
     | 
| 212 | 
         
            +
                },
         
     | 
| 213 | 
         
            +
              });
         
     | 
| 214 | 
         
            +
              sprite.addChild(whiteCircle);
         
     | 
| 215 | 
         
            +
             
     | 
| 216 | 
         
            +
              const ballLabel = Text({
         
     | 
| 217 | 
         
            +
                text: ball.label,
         
     | 
| 218 | 
         
            +
                font: `${ballRadius}px monospace`,
         
     | 
| 219 | 
         
            +
                color: "black",
         
     | 
| 220 | 
         
            +
                anchor: { x: 0.5, y: 0.5 },
         
     | 
| 221 | 
         
            +
                textAlign: "center",
         
     | 
| 222 | 
         
            +
              });
         
     | 
| 223 | 
         
            +
              sprite.addChild(ballLabel);
         
     | 
| 224 | 
         
            +
             
     | 
| 225 | 
         
            +
              ballIdToBallSpriteMap.set(ball.id, sprite);
         
     | 
| 226 | 
         
            +
             
     | 
| 227 | 
         
            +
              if (ball.ownerSocketId === socket.id) setOwnSprite(sprite);
         
     | 
| 228 | 
         
            +
             
     | 
| 229 | 
         
            +
              return sprite;
         
     | 
| 230 | 
         
            +
            }
         
     | 
| 231 | 
         
            +
             
     | 
| 232 | 
         
            +
            function setSpriteVelocity(expectedPosition: Vector, sprite: Sprite) {
         
     | 
| 233 | 
         
            +
              const difference = expectedPosition.subtract(sprite.position);
         
     | 
| 234 | 
         
            +
              sprite.dx = difference.x / gameStateUpdateFramesInterval;
         
     | 
| 235 | 
         
            +
              sprite.dy = difference.y / gameStateUpdateFramesInterval;
         
     | 
| 236 | 
         
            +
            }
         
     | 
| 237 | 
         
            +
             
     | 
| 238 | 
         
            +
            function stopSprite(sprite: Sprite) {
         
     | 
| 239 | 
         
            +
              sprite.ddx = sprite.ddy = sprite.dx = sprite.dy = 0;
         
     | 
| 240 | 
         
            +
            }
         
     | 
| 241 | 
         
            +
             
     | 
| 242 | 
         
            +
            function handleChatMessageReceived(message: string) {
         
     | 
| 243 | 
         
            +
              playSound(messageReceivedSound);
         
     | 
| 244 | 
         
            +
              chatHistory.value += `${getHoursFromLocalTime()}:${getMinutesFromLocalTime()} ${message}\n`;
         
     | 
| 245 | 
         
            +
              if (chatHistory !== document.activeElement) chatHistory.scrollTop = chatHistory.scrollHeight;
         
     | 
| 246 | 
         
            +
            }
         
     | 
| 247 | 
         
            +
             
     | 
| 248 | 
         
            +
            function getMinutesFromLocalTime() {
         
     | 
| 249 | 
         
            +
              return new Date().getMinutes().toString().padStart(2, "0");
         
     | 
| 250 | 
         
            +
            }
         
     | 
| 251 | 
         
            +
             
     | 
| 252 | 
         
            +
            function getHoursFromLocalTime() {
         
     | 
| 253 | 
         
            +
              return new Date().getHours().toString().padStart(2, "0");
         
     | 
| 254 | 
         
            +
            }
         
     | 
| 255 | 
         
            +
             
     | 
| 256 | 
         
            +
            function sendChatMessage() {
         
     | 
| 257 | 
         
            +
              const messageToSend = chatInputField.value.trim();
         
     | 
| 258 | 
         
            +
              chatInputField.value = "";
         
     | 
| 259 | 
         
            +
              if (!messageToSend.length) {
         
     | 
| 260 | 
         
            +
                return;
         
     | 
| 261 | 
         
            +
              } else if (messageToSend.startsWith("/help")) {
         
     | 
| 262 | 
         
            +
                return printHelpText();
         
     | 
| 263 | 
         
            +
              } else if (messageToSend.startsWith("/soundon")) {
         
     | 
| 264 | 
         
            +
                handleChatMessageReceived("📢 Sounds enabled.");
         
     | 
| 265 | 
         
            +
                return publishSoundEnabled(true);
         
     | 
| 266 | 
         
            +
              } else if (messageToSend.startsWith("/soundoff")) {
         
     | 
| 267 | 
         
            +
                handleChatMessageReceived("📢 Sounds disabled.");
         
     | 
| 268 | 
         
            +
                return publishSoundEnabled(false);
         
     | 
| 269 | 
         
            +
              } else {
         
     | 
| 270 | 
         
            +
                socket.emit(ClientToServerEventName.Message, messageToSend);
         
     | 
| 271 | 
         
            +
              }
         
     | 
| 272 | 
         
            +
            }
         
     | 
| 273 | 
         
            +
             
     | 
| 274 | 
         
            +
            function handleKeyPressedOnChatInputField(event: KeyboardEvent) {
         
     | 
| 275 | 
         
            +
              if (event.key === "Enter") sendChatMessage();
         
     | 
| 276 | 
         
            +
            }
         
     | 
| 277 | 
         
            +
             
     | 
| 278 | 
         
            +
            function updateInnerHeightProperty() {
         
     | 
| 279 | 
         
            +
              document.documentElement.style.setProperty("--inner-height", `${window.innerHeight}px`);
         
     | 
| 280 | 
         
            +
            }
         
     | 
| 281 | 
         
            +
             
     | 
| 282 | 
         
            +
            function handleWindowResized() {
         
     | 
| 283 | 
         
            +
              updateInnerHeightProperty();
         
     | 
| 284 | 
         
            +
              fitCanvasInsideItsParent(canvas);
         
     | 
| 285 | 
         
            +
            }
         
     | 
| 286 | 
         
            +
             
     | 
| 287 | 
         
            +
            function playSound(sound: (number | undefined)[]) {
         
     | 
| 288 | 
         
            +
              if (isSoundEnabled()) zzfx(...sound);
         
     | 
| 289 | 
         
            +
            }
         
     | 
| 290 | 
         
            +
             
     | 
| 291 | 
         
            +
            function enableSounds() {
         
     | 
| 292 | 
         
            +
              publishSoundEnabled(true);
         
     | 
| 293 | 
         
            +
              playSound(messageReceivedSound);
         
     | 
| 294 | 
         
            +
            }
         
     | 
| 295 | 
         
            +
             
     | 
| 296 | 
         
            +
            function handlePointerDown() {
         
     | 
| 297 | 
         
            +
              if (!getOwnSprite()) return;
         
     | 
| 298 | 
         
            +
              playSound(acceleratingSound);
         
     | 
| 299 | 
         
            +
              publishPointerPressed(true);
         
     | 
| 300 | 
         
            +
            }
         
     | 
| 301 | 
         
            +
             
     | 
| 302 | 
         
            +
            function handlePointerUp() {
         
     | 
| 303 | 
         
            +
              publishPointerPressed(false);
         
     | 
| 304 | 
         
            +
            }
         
     | 
| 305 | 
         
            +
             
     | 
| 306 | 
         
            +
            function handleObjectDeleted(id: number) {
         
     | 
| 307 | 
         
            +
              const spriteToDelete = ballIdToBallSpriteMap.get(id);
         
     | 
| 308 | 
         
            +
             
     | 
| 309 | 
         
            +
              if (!spriteToDelete) return;
         
     | 
| 310 | 
         
            +
             
     | 
| 311 | 
         
            +
              if (spriteToDelete === getOwnSprite()) setOwnSprite(null);
         
     | 
| 312 | 
         
            +
             
     | 
| 313 | 
         
            +
              ballIdToBallSpriteMap.delete(id);
         
     | 
| 314 | 
         
            +
            }
         
     | 
| 315 | 
         
            +
             
     | 
| 316 | 
         
            +
            function handlePositionsUpdated(positions: BallsPositions): void {
         
     | 
| 317 | 
         
            +
              positions.forEach(([objectId, x, y]) => {
         
     | 
| 318 | 
         
            +
                const sprite = ballIdToBallSpriteMap.get(objectId);
         
     | 
| 319 | 
         
            +
                if (sprite) {
         
     | 
| 320 | 
         
            +
                  const expectedPosition = Vector(x, y);
         
     | 
| 321 | 
         
            +
                  expectedPosition.distance(sprite.position) != 0
         
     | 
| 322 | 
         
            +
                    ? setSpriteVelocity(expectedPosition, sprite)
         
     | 
| 323 | 
         
            +
                    : stopSprite(sprite);
         
     | 
| 324 | 
         
            +
                }
         
     | 
| 325 | 
         
            +
              });
         
     | 
| 326 | 
         
            +
            }
         
     | 
| 327 | 
         
            +
             
     | 
| 328 | 
         
            +
            function handleScoreboardUpdated(overallScoreboard: Scoreboard, tableScoreboard: Scoreboard): void {
         
     | 
| 329 | 
         
            +
              let zeroPaddingLengthForScore = 0;
         
     | 
| 330 | 
         
            +
             
     | 
| 331 | 
         
            +
              if (overallScoreboard[0]) {
         
     | 
| 332 | 
         
            +
                const [, score] = overallScoreboard[0];
         
     | 
| 333 | 
         
            +
                zeroPaddingLengthForScore = score.toString().length;
         
     | 
| 334 | 
         
            +
              }
         
     | 
| 335 | 
         
            +
             
     | 
| 336 | 
         
            +
              const maxNickLength = overallScoreboard.reduce((maxLength, [nick]) => Math.max(maxLength, nick.length), 0);
         
     | 
| 337 | 
         
            +
             
     | 
| 338 | 
         
            +
              scoreboardTextArea.value = "TABLE SCOREBOARD\n\n";
         
     | 
| 339 | 
         
            +
             
     | 
| 340 | 
         
            +
              function writeScore([nick, score, tableId]: [nick: string, score: number, tableId: number]) {
         
     | 
| 341 | 
         
            +
                const formattedScore = String(score).padStart(zeroPaddingLengthForScore, "0");
         
     | 
| 342 | 
         
            +
                const formattedNick = nick.padEnd(maxNickLength, " ");
         
     | 
| 343 | 
         
            +
                scoreboardTextArea.value += `${formattedScore} | ${formattedNick} | Table ${tableId}\n`;
         
     | 
| 344 | 
         
            +
              }
         
     | 
| 345 | 
         
            +
             
     | 
| 346 | 
         
            +
              tableScoreboard.forEach(writeScore);
         
     | 
| 347 | 
         
            +
             
     | 
| 348 | 
         
            +
              scoreboardTextArea.value += "\n\nOVERALL SCOREBOARD\n\n";
         
     | 
| 349 | 
         
            +
             
     | 
| 350 | 
         
            +
              overallScoreboard.forEach(writeScore);
         
     | 
| 351 | 
         
            +
            }
         
     | 
| 352 | 
         
            +
             
     | 
| 353 | 
         
            +
            function handleScoredEvent(value: number, x: number, y: number) {
         
     | 
| 354 | 
         
            +
              playSound(value < 0 ? scoreDecreasedSound : scoreIncreasedSound);
         
     | 
| 355 | 
         
            +
             
     | 
| 356 | 
         
            +
              const scoreText = scoreTextPool.get({
         
     | 
| 357 | 
         
            +
                text: `${value > 0 ? "+" : ""}${value}${value > 0 ? "✨" : "💀"}`,
         
     | 
| 358 | 
         
            +
                font: "36px monospace",
         
     | 
| 359 | 
         
            +
                color: value > 0 ? "#F9D82B" : "#3B3B3B",
         
     | 
| 360 | 
         
            +
                x,
         
     | 
| 361 | 
         
            +
                y,
         
     | 
| 362 | 
         
            +
                anchor: { x: 0.5, y: 0.5 },
         
     | 
| 363 | 
         
            +
                textAlign: "center",
         
     | 
| 364 | 
         
            +
                dy: -1,
         
     | 
| 365 | 
         
            +
                dx: 1,
         
     | 
| 366 | 
         
            +
                update: () => {
         
     | 
| 367 | 
         
            +
                  scoreText.advance();
         
     | 
| 368 | 
         
            +
             
     | 
| 369 | 
         
            +
                  scoreText.opacity -= 0.01;
         
     | 
| 370 | 
         
            +
             
     | 
| 371 | 
         
            +
                  if (scoreText.opacity < 0) scoreText.ttl = 0;
         
     | 
| 372 | 
         
            +
             
     | 
| 373 | 
         
            +
                  if (scoreText.x > x + 2 || scoreText.x < x - 2) scoreText.dx *= -1;
         
     | 
| 374 | 
         
            +
                },
         
     | 
| 375 | 
         
            +
              }) as Text;
         
     | 
| 376 | 
         
            +
            }
         
     | 
| 377 | 
         
            +
             
     | 
| 378 | 
         
            +
            function handlePointerPressed(isPointerPressed: boolean) {
         
     | 
| 379 | 
         
            +
              canvas.style.cursor = isPointerPressed ? "grabbing" : "grab";
         
     | 
| 380 | 
         
            +
            }
         
     | 
| 381 | 
         
            +
             
     | 
| 382 | 
         
            +
            function printWelcomeMessage() {
         
     | 
| 383 | 
         
            +
              return handleChatMessageReceived(
         
     | 
| 384 | 
         
            +
                `👋 Welcome to ${gameName}!\n\nℹ️ New to this game? Enter /help in the message field below to learn about it.\n`
         
     | 
| 385 | 
         
            +
              );
         
     | 
| 386 | 
         
            +
            }
         
     | 
| 387 | 
         
            +
             
     | 
| 388 | 
         
            +
            function updateDocumentTitleWithGameName() {
         
     | 
| 389 | 
         
            +
              document.title = gameName;
         
     | 
| 390 | 
         
            +
            }
         
     | 
| 391 | 
         
            +
             
     | 
| 392 | 
         
            +
            function printHelpText() {
         
     | 
| 393 | 
         
            +
              handleChatMessageReceived(
         
     | 
| 394 | 
         
            +
                `ℹ️ ${gameName} puts you in control of a yoyo on a multiplayer pool table!\n\n` +
         
     | 
| 395 | 
         
            +
                  `The goal is to keep the highest score as long as possible.\n\n` +
         
     | 
| 396 | 
         
            +
                  `Click or touch the table to pull your yoyo.\n\n` +
         
     | 
| 397 | 
         
            +
                  `Each ball has a value, and you should use yoyo maneuvers to bring them into the corner pockets.\n\n` +
         
     | 
| 398 | 
         
            +
                  `If you push another yoyo into a corner pocket, you get part of their score, implying that you also lose part of your score if you end up in a corner pocket.\n\n` +
         
     | 
| 399 | 
         
            +
                  `When the table is clean, balls are brought back to the table. Tip: Focus on pocketing the balls with high value first.\n\n` +
         
     | 
| 400 | 
         
            +
                  `There are several tables in the room, and you can communicate with players from other tables through this chat.\n\n` +
         
     | 
| 401 | 
         
            +
                  `You can also run the following commands here:\n\n` +
         
     | 
| 402 | 
         
            +
                  `Command: /nick <nickname>\n` +
         
     | 
| 403 | 
         
            +
                  `Effect: Changes your nickname.\n\n` +
         
     | 
| 404 | 
         
            +
                  `Command: /newtable\n` +
         
     | 
| 405 | 
         
            +
                  `Effect: Starts a new game on an empty table.\n\n` +
         
     | 
| 406 | 
         
            +
                  `Command: /jointable <number>\n` +
         
     | 
| 407 | 
         
            +
                  `Effect: Joins the game from a specific table.\n\n` +
         
     | 
| 408 | 
         
            +
                  `Command: /soundon\n` +
         
     | 
| 409 | 
         
            +
                  `Effect: Enables sounds.\n\n` +
         
     | 
| 410 | 
         
            +
                  `Command: /soundoff\n` +
         
     | 
| 411 | 
         
            +
                  `Effect: Disables sounds.\n`
         
     | 
| 412 | 
         
            +
              );
         
     | 
| 413 | 
         
            +
            }
         
     | 
| 414 | 
         
            +
             
     | 
| 415 | 
         
            +
            subscribeToMainLoopUpdate(updateScene);
         
     | 
| 416 | 
         
            +
            subscribeToMainLoopDraw(renderScene);
         
     | 
| 417 | 
         
            +
            subscribeToGamePreparationCompleted(startMainLoop);
         
     | 
| 418 | 
         
            +
            subscribeToPageWithImagesLoaded(prepareGame);
         
     | 
| 419 | 
         
            +
            subscribeToPointerPressed(handlePointerPressed);
         
     | 
| 420 | 
         
            +
            onPointer("down", handlePointerDown);
         
     | 
| 421 | 
         
            +
            onPointer("up", handlePointerUp);
         
     | 
| 422 | 
         
            +
            window.addEventListener("load", publishPageWithImagesLoaded);
         
     | 
| 423 | 
         
            +
            window.addEventListener("resize", handleWindowResized);
         
     | 
| 424 | 
         
            +
            window.addEventListener("click", enableSounds, { once: true });
         
     | 
| 425 | 
         
            +
            chatButton.addEventListener("click", sendChatMessage);
         
     | 
| 426 | 
         
            +
            chatInputField.addEventListener("keyup", handleKeyPressedOnChatInputField);
         
     | 
| 427 | 
         
            +
            socket.on(ServerToClientEventName.Message, handleChatMessageReceived);
         
     | 
| 428 | 
         
            +
            socket.on(ServerToClientEventName.Objects, handleBallsPositionsReceived);
         
     | 
| 429 | 
         
            +
            socket.on(ServerToClientEventName.Positions, handlePositionsUpdated);
         
     | 
| 430 | 
         
            +
            socket.on(ServerToClientEventName.Creation, createSpriteForBall);
         
     | 
| 431 | 
         
            +
            socket.on(ServerToClientEventName.Deletion, handleObjectDeleted);
         
     | 
| 432 | 
         
            +
            socket.on(ServerToClientEventName.Scored, handleScoredEvent);
         
     | 
| 433 | 
         
            +
            socket.on(ServerToClientEventName.Scoreboard, handleScoreboardUpdated);
         
     | 
    	
        src/server.ts
    ADDED
    
    | 
         @@ -0,0 +1,513 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import type { Socket } from "socket.io";
         
     | 
| 2 | 
         
            +
            import type { DefaultEventsMap } from "socket.io/dist/typed-events";
         
     | 
| 3 | 
         
            +
            import MainLoop from "mainloop.js";
         
     | 
| 4 | 
         
            +
            import {
         
     | 
| 5 | 
         
            +
              accelerate,
         
     | 
| 6 | 
         
            +
              add,
         
     | 
| 7 | 
         
            +
              collideCircleCircle,
         
     | 
| 8 | 
         
            +
              collideCircleEdge,
         
     | 
| 9 | 
         
            +
              inertia,
         
     | 
| 10 | 
         
            +
              normalize,
         
     | 
| 11 | 
         
            +
              overlapCircleCircle,
         
     | 
| 12 | 
         
            +
              rewindToCollisionPoint,
         
     | 
| 13 | 
         
            +
              sub,
         
     | 
| 14 | 
         
            +
              v2,
         
     | 
| 15 | 
         
            +
              Vector2,
         
     | 
| 16 | 
         
            +
              distance,
         
     | 
| 17 | 
         
            +
              scale,
         
     | 
| 18 | 
         
            +
            } from "pocket-physics";
         
     | 
| 19 | 
         
            +
            import {
         
     | 
| 20 | 
         
            +
              Ball,
         
     | 
| 21 | 
         
            +
              ballsPositionsUpdatesPerSecond,
         
     | 
| 22 | 
         
            +
              squareCanvasSizeInPixels,
         
     | 
| 23 | 
         
            +
              ballRadius,
         
     | 
| 24 | 
         
            +
              ClientToServerEvents,
         
     | 
| 25 | 
         
            +
              ServerToClientEvents,
         
     | 
| 26 | 
         
            +
              ServerToClientEventName,
         
     | 
| 27 | 
         
            +
              ClientToServerEventName,
         
     | 
| 28 | 
         
            +
              BallsPositions,
         
     | 
| 29 | 
         
            +
              Scoreboard,
         
     | 
| 30 | 
         
            +
            } from "./shared";
         
     | 
| 31 | 
         
            +
             
     | 
| 32 | 
         
            +
            type GameSocketData = {
         
     | 
| 33 | 
         
            +
              ball: Ball;
         
     | 
| 34 | 
         
            +
              nickname: string;
         
     | 
| 35 | 
         
            +
              score: number;
         
     | 
| 36 | 
         
            +
              table: Table;
         
     | 
| 37 | 
         
            +
            };
         
     | 
| 38 | 
         
            +
             
     | 
| 39 | 
         
            +
            type GameSocket = Socket<ClientToServerEvents, ServerToClientEvents, DefaultEventsMap, GameSocketData>;
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            type Table = {
         
     | 
| 42 | 
         
            +
              id: number;
         
     | 
| 43 | 
         
            +
              sockets: Map<string, GameSocket>;
         
     | 
| 44 | 
         
            +
              balls: Map<number, Ball>;
         
     | 
| 45 | 
         
            +
            };
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
            let lastScoreboardEmitted = "";
         
     | 
| 48 | 
         
            +
             
     | 
| 49 | 
         
            +
            let uniqueIdCounter = 1;
         
     | 
| 50 | 
         
            +
             
     | 
| 51 | 
         
            +
            let timePassedSinceLastStateUpdateEmitted = 0;
         
     | 
| 52 | 
         
            +
             
     | 
| 53 | 
         
            +
            let timePassedSinceLastScoreboardUpdate = 0;
         
     | 
| 54 | 
         
            +
             
     | 
| 55 | 
         
            +
            const nonPlayableBallsValuesRange = [1, 8] as [min: number, max: number];
         
     | 
| 56 | 
         
            +
             
     | 
| 57 | 
         
            +
            const maxSocketsPerTable = 4;
         
     | 
| 58 | 
         
            +
             
     | 
| 59 | 
         
            +
            const scoreboardUpdateMillisecondsInterval = 1000;
         
     | 
| 60 | 
         
            +
             
     | 
| 61 | 
         
            +
            const objectsPositionsUpdateMillisecondsInterval = 1000 / ballsPositionsUpdatesPerSecond;
         
     | 
| 62 | 
         
            +
             
     | 
| 63 | 
         
            +
            const massOfImmovableObjects = -1;
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
            const tables = new Map<number, Table>();
         
     | 
| 66 | 
         
            +
             
     | 
| 67 | 
         
            +
            const ballColors = ["#fff", "#ffff00", "#0000ff", "#ff0000", "#aa00aa", "#ffaa00", "#1f952f", "#550000", "#1a191e"];
         
     | 
| 68 | 
         
            +
             
     | 
| 69 | 
         
            +
            const collisionDamping = 0.9;
         
     | 
| 70 | 
         
            +
             
     | 
| 71 | 
         
            +
            const cornerPocketSize = 100;
         
     | 
| 72 | 
         
            +
             
     | 
| 73 | 
         
            +
            const tablePadding = 64;
         
     | 
| 74 | 
         
            +
             
     | 
| 75 | 
         
            +
            const maximumNicknameLength = 21;
         
     | 
| 76 | 
         
            +
             
     | 
| 77 | 
         
            +
            const tableLeftRailPoints = [
         
     | 
| 78 | 
         
            +
              v2(tablePadding, cornerPocketSize),
         
     | 
| 79 | 
         
            +
              v2(tablePadding, squareCanvasSizeInPixels - cornerPocketSize),
         
     | 
| 80 | 
         
            +
            ] as [Vector2, Vector2];
         
     | 
| 81 | 
         
            +
             
     | 
| 82 | 
         
            +
            const tableRightRailPoints = [
         
     | 
| 83 | 
         
            +
              v2(squareCanvasSizeInPixels - tablePadding, cornerPocketSize),
         
     | 
| 84 | 
         
            +
              v2(squareCanvasSizeInPixels - tablePadding, squareCanvasSizeInPixels - cornerPocketSize),
         
     | 
| 85 | 
         
            +
            ] as [Vector2, Vector2];
         
     | 
| 86 | 
         
            +
             
     | 
| 87 | 
         
            +
            const tableTopRailPoints = [
         
     | 
| 88 | 
         
            +
              v2(cornerPocketSize, tablePadding),
         
     | 
| 89 | 
         
            +
              v2(squareCanvasSizeInPixels - cornerPocketSize, tablePadding),
         
     | 
| 90 | 
         
            +
            ] as [Vector2, Vector2];
         
     | 
| 91 | 
         
            +
             
     | 
| 92 | 
         
            +
            const tableBottomRailPoints = [
         
     | 
| 93 | 
         
            +
              v2(cornerPocketSize, squareCanvasSizeInPixels - tablePadding),
         
     | 
| 94 | 
         
            +
              v2(squareCanvasSizeInPixels - cornerPocketSize, squareCanvasSizeInPixels - tablePadding),
         
     | 
| 95 | 
         
            +
            ] as [Vector2, Vector2];
         
     | 
| 96 | 
         
            +
             
     | 
| 97 | 
         
            +
            const tableRails = [tableLeftRailPoints, tableRightRailPoints, tableTopRailPoints, tableBottomRailPoints];
         
     | 
| 98 | 
         
            +
             
     | 
| 99 | 
         
            +
            const scoreLineDistanceFromCorner = 140;
         
     | 
| 100 | 
         
            +
             
     | 
| 101 | 
         
            +
            const scoreLines = [
         
     | 
| 102 | 
         
            +
              [v2(0, scoreLineDistanceFromCorner), v2(scoreLineDistanceFromCorner, 0)],
         
     | 
| 103 | 
         
            +
              [
         
     | 
| 104 | 
         
            +
                v2(squareCanvasSizeInPixels - scoreLineDistanceFromCorner, 0),
         
     | 
| 105 | 
         
            +
                v2(squareCanvasSizeInPixels, scoreLineDistanceFromCorner),
         
     | 
| 106 | 
         
            +
              ],
         
     | 
| 107 | 
         
            +
              [
         
     | 
| 108 | 
         
            +
                v2(0, squareCanvasSizeInPixels - scoreLineDistanceFromCorner),
         
     | 
| 109 | 
         
            +
                v2(scoreLineDistanceFromCorner, squareCanvasSizeInPixels),
         
     | 
| 110 | 
         
            +
              ],
         
     | 
| 111 | 
         
            +
              [
         
     | 
| 112 | 
         
            +
                v2(squareCanvasSizeInPixels, squareCanvasSizeInPixels - scoreLineDistanceFromCorner),
         
     | 
| 113 | 
         
            +
                v2(squareCanvasSizeInPixels - scoreLineDistanceFromCorner, squareCanvasSizeInPixels),
         
     | 
| 114 | 
         
            +
              ],
         
     | 
| 115 | 
         
            +
            ] as [Vector2, Vector2][];
         
     | 
| 116 | 
         
            +
             
     | 
| 117 | 
         
            +
            function getUniqueId() {
         
     | 
| 118 | 
         
            +
              const id = uniqueIdCounter;
         
     | 
| 119 | 
         
            +
              id < Number.MAX_SAFE_INTEGER ? uniqueIdCounter++ : 1;
         
     | 
| 120 | 
         
            +
              return id;
         
     | 
| 121 | 
         
            +
            }
         
     | 
| 122 | 
         
            +
             
     | 
| 123 | 
         
            +
            function getRandomElementFrom(object: any[] | string) {
         
     | 
| 124 | 
         
            +
              return object[Math.floor(Math.random() * object.length)];
         
     | 
| 125 | 
         
            +
            }
         
     | 
| 126 | 
         
            +
             
     | 
| 127 | 
         
            +
            function getRandomTextualSmile() {
         
     | 
| 128 | 
         
            +
              return `${getRandomElementFrom(":=")}${getRandomElementFrom("POD)]")}`;
         
     | 
| 129 | 
         
            +
            }
         
     | 
| 130 | 
         
            +
             
     | 
| 131 | 
         
            +
            function addBallToTable(table: Table, properties?: Partial<Ball>) {
         
     | 
| 132 | 
         
            +
              const ball = {
         
     | 
| 133 | 
         
            +
                id: getUniqueId(),
         
     | 
| 134 | 
         
            +
                cpos: v2(),
         
     | 
| 135 | 
         
            +
                ppos: v2(),
         
     | 
| 136 | 
         
            +
                acel: v2(),
         
     | 
| 137 | 
         
            +
                radius: 1,
         
     | 
| 138 | 
         
            +
                mass: 1,
         
     | 
| 139 | 
         
            +
                value: 0,
         
     | 
| 140 | 
         
            +
                label: getRandomTextualSmile(),
         
     | 
| 141 | 
         
            +
                lastTouchedTimestamp: Date.now(),
         
     | 
| 142 | 
         
            +
                ...properties,
         
     | 
| 143 | 
         
            +
              } as Ball;
         
     | 
| 144 | 
         
            +
             
     | 
| 145 | 
         
            +
              placeBallInRandomPosition(ball);
         
     | 
| 146 | 
         
            +
             
     | 
| 147 | 
         
            +
              table.balls.set(ball.id, ball);
         
     | 
| 148 | 
         
            +
             
     | 
| 149 | 
         
            +
              table.sockets.forEach((socket) => socket.emit(ServerToClientEventName.Creation, ball));
         
     | 
| 150 | 
         
            +
             
     | 
| 151 | 
         
            +
              return ball;
         
     | 
| 152 | 
         
            +
            }
         
     | 
| 153 | 
         
            +
             
     | 
| 154 | 
         
            +
            function getRandomPositionForBallOnTable() {
         
     | 
| 155 | 
         
            +
              return (
         
     | 
| 156 | 
         
            +
                tablePadding + ballRadius + Math.floor(Math.random() * (squareCanvasSizeInPixels - (tablePadding + ballRadius) * 2))
         
     | 
| 157 | 
         
            +
              );
         
     | 
| 158 | 
         
            +
            }
         
     | 
| 159 | 
         
            +
             
     | 
| 160 | 
         
            +
            function placeBallInRandomPosition(ball: Ball) {
         
     | 
| 161 | 
         
            +
              const x = getRandomPositionForBallOnTable();
         
     | 
| 162 | 
         
            +
              const y = getRandomPositionForBallOnTable();
         
     | 
| 163 | 
         
            +
              ball.cpos = v2(x, y);
         
     | 
| 164 | 
         
            +
              ball.ppos = v2(x, y);
         
     | 
| 165 | 
         
            +
            }
         
     | 
| 166 | 
         
            +
             
     | 
| 167 | 
         
            +
            function isColliding(firstObject: Ball, secondObject: Ball) {
         
     | 
| 168 | 
         
            +
              return overlapCircleCircle(
         
     | 
| 169 | 
         
            +
                firstObject.cpos.x,
         
     | 
| 170 | 
         
            +
                firstObject.cpos.y,
         
     | 
| 171 | 
         
            +
                firstObject.radius,
         
     | 
| 172 | 
         
            +
                secondObject.cpos.x,
         
     | 
| 173 | 
         
            +
                secondObject.cpos.y,
         
     | 
| 174 | 
         
            +
                secondObject.radius
         
     | 
| 175 | 
         
            +
              );
         
     | 
| 176 | 
         
            +
            }
         
     | 
| 177 | 
         
            +
             
     | 
| 178 | 
         
            +
            function handleCollision(firstObject: Ball, secondObject: Ball) {
         
     | 
| 179 | 
         
            +
              if (firstObject.ownerSocketId || secondObject.ownerSocketId) {
         
     | 
| 180 | 
         
            +
                if (firstObject.ownerSocketId) secondObject.lastTouchedBySocketId = firstObject.ownerSocketId;
         
     | 
| 181 | 
         
            +
             
     | 
| 182 | 
         
            +
                if (secondObject.ownerSocketId) firstObject.lastTouchedBySocketId = secondObject.ownerSocketId;
         
     | 
| 183 | 
         
            +
              } else {
         
     | 
| 184 | 
         
            +
                if (firstObject.lastTouchedTimestamp > secondObject.lastTouchedTimestamp) {
         
     | 
| 185 | 
         
            +
                  secondObject.lastTouchedBySocketId = firstObject.lastTouchedBySocketId;
         
     | 
| 186 | 
         
            +
                } else {
         
     | 
| 187 | 
         
            +
                  firstObject.lastTouchedBySocketId = secondObject.lastTouchedBySocketId;
         
     | 
| 188 | 
         
            +
                }
         
     | 
| 189 | 
         
            +
              }
         
     | 
| 190 | 
         
            +
             
     | 
| 191 | 
         
            +
              firstObject.lastTouchedTimestamp = secondObject.lastTouchedTimestamp = Date.now();
         
     | 
| 192 | 
         
            +
             
     | 
| 193 | 
         
            +
              collideCircleCircle(
         
     | 
| 194 | 
         
            +
                firstObject,
         
     | 
| 195 | 
         
            +
                firstObject.radius,
         
     | 
| 196 | 
         
            +
                firstObject.mass,
         
     | 
| 197 | 
         
            +
                secondObject,
         
     | 
| 198 | 
         
            +
                secondObject.radius,
         
     | 
| 199 | 
         
            +
                secondObject.mass,
         
     | 
| 200 | 
         
            +
                true,
         
     | 
| 201 | 
         
            +
                collisionDamping
         
     | 
| 202 | 
         
            +
              );
         
     | 
| 203 | 
         
            +
            }
         
     | 
| 204 | 
         
            +
             
     | 
| 205 | 
         
            +
            function createBallForSocket(socket: GameSocket) {
         
     | 
| 206 | 
         
            +
              if (!socket.data.table) return;
         
     | 
| 207 | 
         
            +
             
     | 
| 208 | 
         
            +
              socket.data.ball = addBallToTable(socket.data.table, {
         
     | 
| 209 | 
         
            +
                radius: ballRadius,
         
     | 
| 210 | 
         
            +
                ownerSocketId: socket.id,
         
     | 
| 211 | 
         
            +
                color: getRandomHexColor(),
         
     | 
| 212 | 
         
            +
                value: 9,
         
     | 
| 213 | 
         
            +
              });
         
     | 
| 214 | 
         
            +
            }
         
     | 
| 215 | 
         
            +
             
     | 
| 216 | 
         
            +
            function deleteBallFromSocket(socket: GameSocket) {
         
     | 
| 217 | 
         
            +
              if (!socket.data.table || !socket.data.ball) return;
         
     | 
| 218 | 
         
            +
             
     | 
| 219 | 
         
            +
              deleteBallFromTable(socket.data.ball, socket.data.table);
         
     | 
| 220 | 
         
            +
             
     | 
| 221 | 
         
            +
              socket.data.ball = undefined;
         
     | 
| 222 | 
         
            +
            }
         
     | 
| 223 | 
         
            +
             
     | 
| 224 | 
         
            +
            function getNumberOfNonPlayableBallsOnTable(table: Table) {
         
     | 
| 225 | 
         
            +
              return Array.from(table.balls.values()).filter((ball) => !ball.ownerSocketId).length;
         
     | 
| 226 | 
         
            +
            }
         
     | 
| 227 | 
         
            +
             
     | 
| 228 | 
         
            +
            function handleSocketConnected(socket: GameSocket) {
         
     | 
| 229 | 
         
            +
              socket.data.nickname = `Player ${getUniqueId()}`;
         
     | 
| 230 | 
         
            +
              socket.data.score = 0;
         
     | 
| 231 | 
         
            +
             
     | 
| 232 | 
         
            +
              const table =
         
     | 
| 233 | 
         
            +
                Array.from(tables.values()).find((currentTable) => currentTable.sockets.size < maxSocketsPerTable) ?? createTable();
         
     | 
| 234 | 
         
            +
             
     | 
| 235 | 
         
            +
              addSocketToTable(socket, table);
         
     | 
| 236 | 
         
            +
             
     | 
| 237 | 
         
            +
              setupSocketListeners(socket);
         
     | 
| 238 | 
         
            +
            }
         
     | 
| 239 | 
         
            +
             
     | 
| 240 | 
         
            +
            function handleSocketDisconnected(socket: GameSocket) {
         
     | 
| 241 | 
         
            +
              if (!socket.data.table) return;
         
     | 
| 242 | 
         
            +
              removeSocketFromTable(socket, socket.data.table);
         
     | 
| 243 | 
         
            +
            }
         
     | 
| 244 | 
         
            +
             
     | 
| 245 | 
         
            +
            function broadcastChatMessageToTable(message: string, table: Table) {
         
     | 
| 246 | 
         
            +
              return table.sockets.forEach((socket) => socket.emit(ServerToClientEventName.Message, message));
         
     | 
| 247 | 
         
            +
            }
         
     | 
| 248 | 
         
            +
             
     | 
| 249 | 
         
            +
            function broadcastChatMessageToAllTables(message: string) {
         
     | 
| 250 | 
         
            +
              return tables.forEach((table) => broadcastChatMessageToTable(message, table));
         
     | 
| 251 | 
         
            +
            }
         
     | 
| 252 | 
         
            +
             
     | 
| 253 | 
         
            +
            function accelerateBallFromSocket(x: number, y: number, socket: GameSocket) {
         
     | 
| 254 | 
         
            +
              if (!socket.data.ball) return;
         
     | 
| 255 | 
         
            +
              const accelerationVector = v2();
         
     | 
| 256 | 
         
            +
              sub(accelerationVector, v2(x, y), socket.data.ball.cpos);
         
     | 
| 257 | 
         
            +
              normalize(accelerationVector, accelerationVector);
         
     | 
| 258 | 
         
            +
              const elasticityFactor = 20 * (distance(v2(x, y), socket.data.ball.cpos) / squareCanvasSizeInPixels);
         
     | 
| 259 | 
         
            +
              scale(accelerationVector, accelerationVector, elasticityFactor);
         
     | 
| 260 | 
         
            +
              add(socket.data.ball.acel, socket.data.ball.acel, accelerationVector);
         
     | 
| 261 | 
         
            +
            }
         
     | 
| 262 | 
         
            +
             
     | 
| 263 | 
         
            +
            function handleMessageReceivedFromSocket(message: string, socket: GameSocket) {
         
     | 
| 264 | 
         
            +
              if (message.startsWith("/nick ")) {
         
     | 
| 265 | 
         
            +
                const trimmedNickname = message.replace("/nick ", "").trim().substring(0, maximumNicknameLength);
         
     | 
| 266 | 
         
            +
             
     | 
| 267 | 
         
            +
                if (trimmedNickname.length) {
         
     | 
| 268 | 
         
            +
                  broadcastChatMessageToAllTables(`📢 ${socket.data.nickname} is now known as ${trimmedNickname}!`);
         
     | 
| 269 | 
         
            +
                  socket.data.nickname = trimmedNickname;
         
     | 
| 270 | 
         
            +
                }
         
     | 
| 271 | 
         
            +
              } else if (message.startsWith("/newtable")) {
         
     | 
| 272 | 
         
            +
                removeSocketFromTable(socket, socket.data.table);
         
     | 
| 273 | 
         
            +
                addSocketToTable(socket, createTable());
         
     | 
| 274 | 
         
            +
              } else if (message.startsWith("/jointable ")) {
         
     | 
| 275 | 
         
            +
                const tableId = Number(message.replace("/jointable ", "").trim());
         
     | 
| 276 | 
         
            +
             
     | 
| 277 | 
         
            +
                if (isNaN(tableId) || !tables.has(tableId)) {
         
     | 
| 278 | 
         
            +
                  socket.emit(ServerToClientEventName.Message, `📢 Table not found!`);
         
     | 
| 279 | 
         
            +
                } else if (tables.get(tableId) === socket.data.table) {
         
     | 
| 280 | 
         
            +
                  socket.emit(ServerToClientEventName.Message, `📢 Already on table ${tableId}!`);
         
     | 
| 281 | 
         
            +
                } else if ((tables.get(tableId) as Table).sockets.size >= maxSocketsPerTable) {
         
     | 
| 282 | 
         
            +
                  socket.emit(ServerToClientEventName.Message, `📢 Table is full!`);
         
     | 
| 283 | 
         
            +
                } else {
         
     | 
| 284 | 
         
            +
                  removeSocketFromTable(socket, socket.data.table);
         
     | 
| 285 | 
         
            +
                  addSocketToTable(socket, tables.get(tableId) as Table);
         
     | 
| 286 | 
         
            +
                }
         
     | 
| 287 | 
         
            +
              } else {
         
     | 
| 288 | 
         
            +
                broadcastChatMessageToAllTables(`💬 ${socket.data.nickname}: ${message}`);
         
     | 
| 289 | 
         
            +
              }
         
     | 
| 290 | 
         
            +
            }
         
     | 
| 291 | 
         
            +
             
     | 
| 292 | 
         
            +
            function setupSocketListeners(socket: GameSocket) {
         
     | 
| 293 | 
         
            +
              socket.on("disconnect", () => handleSocketDisconnected(socket));
         
     | 
| 294 | 
         
            +
              socket.on(ClientToServerEventName.Message, (message) => handleMessageReceivedFromSocket(message, socket));
         
     | 
| 295 | 
         
            +
              socket.on(ClientToServerEventName.Click, (x, y) => accelerateBallFromSocket(x, y, socket));
         
     | 
| 296 | 
         
            +
            }
         
     | 
| 297 | 
         
            +
             
     | 
| 298 | 
         
            +
            function checkCollisionWithTableRails(ball: Ball) {
         
     | 
| 299 | 
         
            +
              tableRails.forEach(([pointA, pointB]) => {
         
     | 
| 300 | 
         
            +
                if (rewindToCollisionPoint(ball, ball.radius, pointA, pointB))
         
     | 
| 301 | 
         
            +
                  collideCircleEdge(
         
     | 
| 302 | 
         
            +
                    ball,
         
     | 
| 303 | 
         
            +
                    ball.radius,
         
     | 
| 304 | 
         
            +
                    ball.mass,
         
     | 
| 305 | 
         
            +
                    {
         
     | 
| 306 | 
         
            +
                      cpos: pointA,
         
     | 
| 307 | 
         
            +
                      ppos: pointA,
         
     | 
| 308 | 
         
            +
                    },
         
     | 
| 309 | 
         
            +
                    massOfImmovableObjects,
         
     | 
| 310 | 
         
            +
                    {
         
     | 
| 311 | 
         
            +
                      cpos: pointB,
         
     | 
| 312 | 
         
            +
                      ppos: pointB,
         
     | 
| 313 | 
         
            +
                    },
         
     | 
| 314 | 
         
            +
                    massOfImmovableObjects,
         
     | 
| 315 | 
         
            +
                    true,
         
     | 
| 316 | 
         
            +
                    collisionDamping
         
     | 
| 317 | 
         
            +
                  );
         
     | 
| 318 | 
         
            +
              });
         
     | 
| 319 | 
         
            +
            }
         
     | 
| 320 | 
         
            +
             
     | 
| 321 | 
         
            +
            function deleteBallFromTable(ball: Ball, table: Table) {
         
     | 
| 322 | 
         
            +
              if (table.balls.has(ball.id)) {
         
     | 
| 323 | 
         
            +
                table.balls.delete(ball.id);
         
     | 
| 324 | 
         
            +
                table.sockets.forEach((targetSocket) => targetSocket.emit(ServerToClientEventName.Deletion, ball.id));
         
     | 
| 325 | 
         
            +
              }
         
     | 
| 326 | 
         
            +
             
     | 
| 327 | 
         
            +
              if (getNumberOfNonPlayableBallsOnTable(table) == 0) addNonPlayableBallsToTable(table);
         
     | 
| 328 | 
         
            +
            }
         
     | 
| 329 | 
         
            +
             
     | 
| 330 | 
         
            +
            function checkCollisionWithScoreLines(ball: Ball, table: Table) {
         
     | 
| 331 | 
         
            +
              scoreLines.forEach(([pointA, pointB]) => {
         
     | 
| 332 | 
         
            +
                if (rewindToCollisionPoint(ball, ball.radius, pointA, pointB)) {
         
     | 
| 333 | 
         
            +
                  deleteBallFromTable(ball, table);
         
     | 
| 334 | 
         
            +
             
     | 
| 335 | 
         
            +
                  if (ball.ownerSocketId) {
         
     | 
| 336 | 
         
            +
                    const socket = table.sockets.get(ball.ownerSocketId);
         
     | 
| 337 | 
         
            +
             
     | 
| 338 | 
         
            +
                    if (socket) {
         
     | 
| 339 | 
         
            +
                      const negativeScore = -ball.value;
         
     | 
| 340 | 
         
            +
                      socket.data.score = Math.max(0, (socket.data.score as number) + negativeScore);
         
     | 
| 341 | 
         
            +
                      socket.emit(ServerToClientEventName.Scored, negativeScore, ball.cpos.x, ball.cpos.y);
         
     | 
| 342 | 
         
            +
                      createBallForSocket(socket);
         
     | 
| 343 | 
         
            +
                    }
         
     | 
| 344 | 
         
            +
                  }
         
     | 
| 345 | 
         
            +
             
     | 
| 346 | 
         
            +
                  if (ball.lastTouchedBySocketId) {
         
     | 
| 347 | 
         
            +
                    const socket = table.sockets.get(ball.lastTouchedBySocketId);
         
     | 
| 348 | 
         
            +
             
     | 
| 349 | 
         
            +
                    if (socket) {
         
     | 
| 350 | 
         
            +
                      socket.data.score = (socket.data.score as number) + ball.value;
         
     | 
| 351 | 
         
            +
                      socket.emit(ServerToClientEventName.Scored, ball.value, ball.cpos.x, ball.cpos.y);
         
     | 
| 352 | 
         
            +
                    }
         
     | 
| 353 | 
         
            +
                  }
         
     | 
| 354 | 
         
            +
                }
         
     | 
| 355 | 
         
            +
              });
         
     | 
| 356 | 
         
            +
            }
         
     | 
| 357 | 
         
            +
             
     | 
| 358 | 
         
            +
            function emitObjectsPositionsToConnectedSockets() {
         
     | 
| 359 | 
         
            +
              Array.from(tables.values())
         
     | 
| 360 | 
         
            +
                .filter((table) => table.balls.size)
         
     | 
| 361 | 
         
            +
                .forEach((table) => {
         
     | 
| 362 | 
         
            +
                  const positions = Array.from(table.balls.values()).reduce<BallsPositions>((resultArray, ball) => {
         
     | 
| 363 | 
         
            +
                    resultArray.push([ball.id, Math.trunc(ball.cpos.x), Math.trunc(ball.cpos.y)]);
         
     | 
| 364 | 
         
            +
                    return resultArray;
         
     | 
| 365 | 
         
            +
                  }, []);
         
     | 
| 366 | 
         
            +
             
     | 
| 367 | 
         
            +
                  table.sockets.forEach((socket) => {
         
     | 
| 368 | 
         
            +
                    socket.emit(ServerToClientEventName.Positions, positions);
         
     | 
| 369 | 
         
            +
                  });
         
     | 
| 370 | 
         
            +
                });
         
     | 
| 371 | 
         
            +
            }
         
     | 
| 372 | 
         
            +
             
     | 
| 373 | 
         
            +
            function emitScoreboardToConnectedSockets() {
         
     | 
| 374 | 
         
            +
              const tableIdPerScoreboardMap = new Map<number, Scoreboard>();
         
     | 
| 375 | 
         
            +
             
     | 
| 376 | 
         
            +
              tables.forEach((table) => {
         
     | 
| 377 | 
         
            +
                const tableScoreboard = Array.from(table.sockets.values())
         
     | 
| 378 | 
         
            +
                  .sort((a, b) => (b.data.score as number) - (a.data.score as number))
         
     | 
| 379 | 
         
            +
                  .reduce<Scoreboard>((scoreboard, socket) => {
         
     | 
| 380 | 
         
            +
                    scoreboard.push([socket.data.nickname as string, socket.data.score as number, table.id as number]);
         
     | 
| 381 | 
         
            +
                    return scoreboard;
         
     | 
| 382 | 
         
            +
                  }, []);
         
     | 
| 383 | 
         
            +
             
     | 
| 384 | 
         
            +
                tableIdPerScoreboardMap.set(table.id, tableScoreboard);
         
     | 
| 385 | 
         
            +
              });
         
     | 
| 386 | 
         
            +
             
     | 
| 387 | 
         
            +
              const overallScoreboard = [] as Scoreboard;
         
     | 
| 388 | 
         
            +
             
     | 
| 389 | 
         
            +
              tableIdPerScoreboardMap.forEach((tableScoreboard) => overallScoreboard.push(...tableScoreboard));
         
     | 
| 390 | 
         
            +
             
     | 
| 391 | 
         
            +
              overallScoreboard.sort(([, scoreA], [, scoreB]) => scoreB - scoreA);
         
     | 
| 392 | 
         
            +
             
     | 
| 393 | 
         
            +
              const scoreBoardToEmit = JSON.stringify(overallScoreboard);
         
     | 
| 394 | 
         
            +
             
     | 
| 395 | 
         
            +
              if (lastScoreboardEmitted === scoreBoardToEmit) return;
         
     | 
| 396 | 
         
            +
             
     | 
| 397 | 
         
            +
              lastScoreboardEmitted = scoreBoardToEmit;
         
     | 
| 398 | 
         
            +
             
     | 
| 399 | 
         
            +
              tables.forEach((table) => {
         
     | 
| 400 | 
         
            +
                table.sockets.forEach((socket) => {
         
     | 
| 401 | 
         
            +
                  let tableScoreboard = [] as Scoreboard;
         
     | 
| 402 | 
         
            +
                  if (socket.data.table && tableIdPerScoreboardMap.has(socket.data.table.id)) {
         
     | 
| 403 | 
         
            +
                    tableScoreboard = tableIdPerScoreboardMap.get(socket.data.table.id) as Scoreboard;
         
     | 
| 404 | 
         
            +
                  }
         
     | 
| 405 | 
         
            +
                  socket.emit(ServerToClientEventName.Scoreboard, overallScoreboard, tableScoreboard);
         
     | 
| 406 | 
         
            +
                });
         
     | 
| 407 | 
         
            +
              });
         
     | 
| 408 | 
         
            +
            }
         
     | 
| 409 | 
         
            +
             
     | 
| 410 | 
         
            +
            function repositionBallIfItIsOutOfTable(ball: Ball) {
         
     | 
| 411 | 
         
            +
              if (
         
     | 
| 412 | 
         
            +
                ball.cpos.x < 0 ||
         
     | 
| 413 | 
         
            +
                ball.cpos.x > squareCanvasSizeInPixels ||
         
     | 
| 414 | 
         
            +
                ball.cpos.y < 0 ||
         
     | 
| 415 | 
         
            +
                ball.cpos.y > squareCanvasSizeInPixels
         
     | 
| 416 | 
         
            +
              ) {
         
     | 
| 417 | 
         
            +
                placeBallInRandomPosition(ball);
         
     | 
| 418 | 
         
            +
              }
         
     | 
| 419 | 
         
            +
            }
         
     | 
| 420 | 
         
            +
             
     | 
| 421 | 
         
            +
            function updatePhysics(deltaTime: number) {
         
     | 
| 422 | 
         
            +
              tables.forEach((table) => {
         
     | 
| 423 | 
         
            +
                Array.from(table.balls.values()).forEach((ball, _, balls) => {
         
     | 
| 424 | 
         
            +
                  repositionBallIfItIsOutOfTable(ball);
         
     | 
| 425 | 
         
            +
             
     | 
| 426 | 
         
            +
                  accelerate(ball, deltaTime);
         
     | 
| 427 | 
         
            +
             
     | 
| 428 | 
         
            +
                  balls
         
     | 
| 429 | 
         
            +
                    .filter((otherBalls) => ball !== otherBalls && isColliding(ball, otherBalls))
         
     | 
| 430 | 
         
            +
                    .forEach((otherBall) => handleCollision(ball, otherBall));
         
     | 
| 431 | 
         
            +
             
     | 
| 432 | 
         
            +
                  checkCollisionWithTableRails(ball);
         
     | 
| 433 | 
         
            +
             
     | 
| 434 | 
         
            +
                  checkCollisionWithScoreLines(ball, table);
         
     | 
| 435 | 
         
            +
             
     | 
| 436 | 
         
            +
                  inertia(ball);
         
     | 
| 437 | 
         
            +
                });
         
     | 
| 438 | 
         
            +
              });
         
     | 
| 439 | 
         
            +
            }
         
     | 
| 440 | 
         
            +
             
     | 
| 441 | 
         
            +
            function getRandomHexColor() {
         
     | 
| 442 | 
         
            +
              const randomInteger = (max: number) => Math.floor(Math.random() * (max + 1));
         
     | 
| 443 | 
         
            +
              const randomRgbColor = () => [randomInteger(255), randomInteger(255), randomInteger(255)];
         
     | 
| 444 | 
         
            +
              const [r, g, b] = randomRgbColor();
         
     | 
| 445 | 
         
            +
              return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
         
     | 
| 446 | 
         
            +
            }
         
     | 
| 447 | 
         
            +
             
     | 
| 448 | 
         
            +
            function handleMainLoopUpdate(deltaTime: number) {
         
     | 
| 449 | 
         
            +
              updatePhysics(deltaTime);
         
     | 
| 450 | 
         
            +
             
     | 
| 451 | 
         
            +
              timePassedSinceLastStateUpdateEmitted += deltaTime;
         
     | 
| 452 | 
         
            +
              if (timePassedSinceLastStateUpdateEmitted > objectsPositionsUpdateMillisecondsInterval) {
         
     | 
| 453 | 
         
            +
                timePassedSinceLastStateUpdateEmitted -= objectsPositionsUpdateMillisecondsInterval;
         
     | 
| 454 | 
         
            +
                emitObjectsPositionsToConnectedSockets();
         
     | 
| 455 | 
         
            +
              }
         
     | 
| 456 | 
         
            +
             
     | 
| 457 | 
         
            +
              timePassedSinceLastScoreboardUpdate += deltaTime;
         
     | 
| 458 | 
         
            +
              if (timePassedSinceLastScoreboardUpdate > scoreboardUpdateMillisecondsInterval) {
         
     | 
| 459 | 
         
            +
                timePassedSinceLastScoreboardUpdate -= scoreboardUpdateMillisecondsInterval;
         
     | 
| 460 | 
         
            +
                emitScoreboardToConnectedSockets();
         
     | 
| 461 | 
         
            +
              }
         
     | 
| 462 | 
         
            +
            }
         
     | 
| 463 | 
         
            +
             
     | 
| 464 | 
         
            +
            function addNonPlayableBallsToTable(table: Table) {
         
     | 
| 465 | 
         
            +
              const [min, max] = nonPlayableBallsValuesRange;
         
     | 
| 466 | 
         
            +
              for (let value = min; value <= max; value++) {
         
     | 
| 467 | 
         
            +
                addBallToTable(table, {
         
     | 
| 468 | 
         
            +
                  radius: ballRadius,
         
     | 
| 469 | 
         
            +
                  value,
         
     | 
| 470 | 
         
            +
                  label: `${value}`,
         
     | 
| 471 | 
         
            +
                  color: ballColors[value],
         
     | 
| 472 | 
         
            +
                });
         
     | 
| 473 | 
         
            +
              }
         
     | 
| 474 | 
         
            +
            }
         
     | 
| 475 | 
         
            +
             
     | 
| 476 | 
         
            +
            function addSocketToTable(socket: GameSocket, table: Table) {
         
     | 
| 477 | 
         
            +
              table.sockets.set(socket.id, socket);
         
     | 
| 478 | 
         
            +
              socket.data.table = table;
         
     | 
| 479 | 
         
            +
              createBallForSocket(socket);
         
     | 
| 480 | 
         
            +
              socket.emit(ServerToClientEventName.Objects, Array.from(table.balls.values()));
         
     | 
| 481 | 
         
            +
              broadcastChatMessageToAllTables(`📢 ${socket.data.nickname} joined Table ${table.id}!`);
         
     | 
| 482 | 
         
            +
            }
         
     | 
| 483 | 
         
            +
             
     | 
| 484 | 
         
            +
            function removeSocketFromTable(socket: GameSocket, table?: Table) {
         
     | 
| 485 | 
         
            +
              if (!table) return;
         
     | 
| 486 | 
         
            +
              deleteBallFromSocket(socket);
         
     | 
| 487 | 
         
            +
              table.sockets.delete(socket.id);
         
     | 
| 488 | 
         
            +
              socket.data.table = undefined;
         
     | 
| 489 | 
         
            +
              if (!table.sockets.size) deleteTable(table);
         
     | 
| 490 | 
         
            +
            }
         
     | 
| 491 | 
         
            +
             
     | 
| 492 | 
         
            +
            function createTable() {
         
     | 
| 493 | 
         
            +
              const table = {
         
     | 
| 494 | 
         
            +
                id: getUniqueId(),
         
     | 
| 495 | 
         
            +
                sockets: new Map<string, GameSocket>(),
         
     | 
| 496 | 
         
            +
                balls: new Map<number, Ball>(),
         
     | 
| 497 | 
         
            +
              } as Table;
         
     | 
| 498 | 
         
            +
             
     | 
| 499 | 
         
            +
              tables.set(table.id, table);
         
     | 
| 500 | 
         
            +
             
     | 
| 501 | 
         
            +
              addNonPlayableBallsToTable(table);
         
     | 
| 502 | 
         
            +
             
     | 
| 503 | 
         
            +
              return table;
         
     | 
| 504 | 
         
            +
            }
         
     | 
| 505 | 
         
            +
             
     | 
| 506 | 
         
            +
            function deleteTable(table: Table) {
         
     | 
| 507 | 
         
            +
              Array.from(table.balls.values()).forEach((ball) => deleteBallFromTable(ball, table));
         
     | 
| 508 | 
         
            +
              tables.delete(table.id);
         
     | 
| 509 | 
         
            +
            }
         
     | 
| 510 | 
         
            +
             
     | 
| 511 | 
         
            +
            MainLoop.setUpdate(handleMainLoopUpdate).start();
         
     | 
| 512 | 
         
            +
             
     | 
| 513 | 
         
            +
            export default { io: handleSocketConnected };
         
     | 
    	
        src/shared.ts
    ADDED
    
    | 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import type { Integratable } from "pocket-physics";
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            export const ballsPositionsUpdatesPerSecond = 8;
         
     | 
| 4 | 
         
            +
             
     | 
| 5 | 
         
            +
            export const ballRadius = 14;
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            export const squareCanvasSizeInPixels = 680;
         
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            export type Ball = Integratable & {
         
     | 
| 10 | 
         
            +
              id: number;
         
     | 
| 11 | 
         
            +
              radius: number;
         
     | 
| 12 | 
         
            +
              mass: number;
         
     | 
| 13 | 
         
            +
              value: number;
         
     | 
| 14 | 
         
            +
              label: string;
         
     | 
| 15 | 
         
            +
              color: string;
         
     | 
| 16 | 
         
            +
              lastTouchedTimestamp: number;
         
     | 
| 17 | 
         
            +
              lastTouchedBySocketId?: string;
         
     | 
| 18 | 
         
            +
              ownerSocketId?: string;
         
     | 
| 19 | 
         
            +
            };
         
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
            export type BallsPositions = [objectId: number, x: number, y: number][];
         
     | 
| 22 | 
         
            +
             
     | 
| 23 | 
         
            +
            export type Scoreboard = [nick: string, score: number, tableId: number][];
         
     | 
| 24 | 
         
            +
             
     | 
| 25 | 
         
            +
            export enum ServerToClientEventName {
         
     | 
| 26 | 
         
            +
              Message = "A",
         
     | 
| 27 | 
         
            +
              Objects = "B",
         
     | 
| 28 | 
         
            +
              Creation = "C",
         
     | 
| 29 | 
         
            +
              Deletion = "D",
         
     | 
| 30 | 
         
            +
              Scored = "E",
         
     | 
| 31 | 
         
            +
              Positions = "F",
         
     | 
| 32 | 
         
            +
              Scoreboard = "G",
         
     | 
| 33 | 
         
            +
            }
         
     | 
| 34 | 
         
            +
             
     | 
| 35 | 
         
            +
            export enum ClientToServerEventName {
         
     | 
| 36 | 
         
            +
              Message = "A",
         
     | 
| 37 | 
         
            +
              Click = "B",
         
     | 
| 38 | 
         
            +
            }
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
            export interface ServerToClientEvents {
         
     | 
| 41 | 
         
            +
              [ServerToClientEventName.Message]: (message: string) => void;
         
     | 
| 42 | 
         
            +
              [ServerToClientEventName.Objects]: (objects: Ball[]) => void;
         
     | 
| 43 | 
         
            +
              [ServerToClientEventName.Creation]: (object: Ball) => void;
         
     | 
| 44 | 
         
            +
              [ServerToClientEventName.Deletion]: (id: number) => void;
         
     | 
| 45 | 
         
            +
              [ServerToClientEventName.Scored]: (value: number, positionX: number, positionY: number) => void;
         
     | 
| 46 | 
         
            +
              [ServerToClientEventName.Positions]: (ballsPositions: BallsPositions) => void;
         
     | 
| 47 | 
         
            +
              [ServerToClientEventName.Scoreboard]: (overallScoreboard: Scoreboard, tableScoreboard: Scoreboard) => void;
         
     | 
| 48 | 
         
            +
            }
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
            export interface ClientToServerEvents {
         
     | 
| 51 | 
         
            +
              [ClientToServerEventName.Message]: (message: string) => void;
         
     | 
| 52 | 
         
            +
              [ClientToServerEventName.Click]: (positionX: number, positionY: number) => void;
         
     | 
| 53 | 
         
            +
            }
         
     | 
    	
        src/types.d.ts
    ADDED
    
    | 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            declare const io: typeof import("socket.io-client").io;
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            declare module "zzfx" {
         
     | 
| 4 | 
         
            +
              export function zzfx(
         
     | 
| 5 | 
         
            +
                volume?: number,
         
     | 
| 6 | 
         
            +
                randomness?: number,
         
     | 
| 7 | 
         
            +
                frequency?: number,
         
     | 
| 8 | 
         
            +
                attack?: number,
         
     | 
| 9 | 
         
            +
                sustain?: number,
         
     | 
| 10 | 
         
            +
                release?: number,
         
     | 
| 11 | 
         
            +
                shape?: number,
         
     | 
| 12 | 
         
            +
                shapeCurve?: number,
         
     | 
| 13 | 
         
            +
                slide?: number,
         
     | 
| 14 | 
         
            +
                deltaSlide?: number,
         
     | 
| 15 | 
         
            +
                pitchJump?: number,
         
     | 
| 16 | 
         
            +
                pitchJumpTime?: number,
         
     | 
| 17 | 
         
            +
                repeatTime?: number,
         
     | 
| 18 | 
         
            +
                noise?: number,
         
     | 
| 19 | 
         
            +
                modulation?: number,
         
     | 
| 20 | 
         
            +
                bitCrush?: number,
         
     | 
| 21 | 
         
            +
                delay?: number,
         
     | 
| 22 | 
         
            +
                sustainVolume?: number,
         
     | 
| 23 | 
         
            +
                decay?: number,
         
     | 
| 24 | 
         
            +
                tremolo?: number
         
     | 
| 25 | 
         
            +
              ): AudioBufferSourceNode;
         
     | 
| 26 | 
         
            +
            }
         
     | 
    	
        tsconfig.json
    ADDED
    
    | 
         @@ -0,0 +1,11 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            {
         
     | 
| 2 | 
         
            +
              "compilerOptions": {
         
     | 
| 3 | 
         
            +
                "target": "ESNext",
         
     | 
| 4 | 
         
            +
                "moduleResolution": "node",
         
     | 
| 5 | 
         
            +
                "allowSyntheticDefaultImports": true,
         
     | 
| 6 | 
         
            +
                "esModuleInterop": true,
         
     | 
| 7 | 
         
            +
                "forceConsistentCasingInFileNames": true,
         
     | 
| 8 | 
         
            +
                "strict": true,
         
     | 
| 9 | 
         
            +
                "skipLibCheck": true
         
     | 
| 10 | 
         
            +
              }
         
     | 
| 11 | 
         
            +
            }
         
     |