From 25320b826c291a8c0c70b78383852d850cdaabeb Mon Sep 17 00:00:00 2001 From: vvandenb Date: Thu, 16 Feb 2023 17:47:44 +0100 Subject: [PATCH] * Added typescript modules & typescript config * Formatted & typescripted svelte files * Added pong game --- package.json | 3 + rollup.config.js | 130 ++++++++++--------- src/App.svelte | 94 +++++++------- src/components/Friends.svelte | 137 +++++++++++--------- src/components/MatchHistory.svelte | 95 +++++++------- src/components/NavBar.svelte | 184 +++++++++++++------------- src/components/Play.svelte | 57 ++++---- src/components/Pong/Ball.ts | 15 +++ src/components/Pong/Game.ts | 80 ++++++++++++ src/components/Pong/Paddle.ts | 29 +++++ src/components/Pong/Player.ts | 19 +++ src/components/Pong/Pong.svelte | 65 ++++++++++ src/components/Pong/constants.ts | 32 +++++ src/components/Pong/pong.ts | 52 ++++++++ src/components/Pong/utils.ts | 88 +++++++++++++ src/components/Profile.svelte | 200 ++++++++++++++--------------- src/components/Spectate.svelte | 87 +++++++------ tsconfig.json | 52 ++++---- 18 files changed, 926 insertions(+), 493 deletions(-) create mode 100644 src/components/Pong/Ball.ts create mode 100644 src/components/Pong/Game.ts create mode 100644 src/components/Pong/Paddle.ts create mode 100644 src/components/Pong/Player.ts create mode 100644 src/components/Pong/Pong.svelte create mode 100644 src/components/Pong/constants.ts create mode 100644 src/components/Pong/pong.ts create mode 100644 src/components/Pong/utils.ts diff --git a/package.json b/package.json index e6549c7..0470527 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,10 @@ "svelte": "^3.0.0" }, "dependencies": { + "@rollup/plugin-typescript": "^11.0.0", "sirv-cli": "^2.0.0", + "svelte-check": "^3.0.3", + "svelte-preprocess": "^5.0.1", "svelte-routing": "^1.6.0" } } diff --git a/rollup.config.js b/rollup.config.js index 01637f4..4b75ee7 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,76 +1,84 @@ -import svelte from 'rollup-plugin-svelte'; -import commonjs from '@rollup/plugin-commonjs'; -import resolve from '@rollup/plugin-node-resolve'; -import livereload from 'rollup-plugin-livereload'; -import { terser } from 'rollup-plugin-terser'; -import css from 'rollup-plugin-css-only'; +import svelte from "rollup-plugin-svelte"; +import commonjs from "@rollup/plugin-commonjs"; +import resolve from "@rollup/plugin-node-resolve"; +import livereload from "rollup-plugin-livereload"; +import { terser } from "rollup-plugin-terser"; +import css from "rollup-plugin-css-only"; +import autoPreprocess from "svelte-preprocess"; +import typescript from "@rollup/plugin-typescript"; const production = !process.env.ROLLUP_WATCH; function serve() { - let server; + let server; - function toExit() { - if (server) server.kill(0); - } + function toExit() { + if (server) server.kill(0); + } - return { - writeBundle() { - if (server) return; - server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { - stdio: ['ignore', 'inherit', 'inherit'], - shell: true - }); + return { + writeBundle() { + if (server) return; + server = require("child_process").spawn( + "npm", + ["run", "start", "--", "--dev"], + { + stdio: ["ignore", "inherit", "inherit"], + shell: true, + } + ); - process.on('SIGTERM', toExit); - process.on('exit', toExit); - } - }; + process.on("SIGTERM", toExit); + process.on("exit", toExit); + }, + }; } export default { - input: 'src/main.ts', - output: { - sourcemap: true, - format: 'iife', - name: 'app', - file: 'public/build/bundle.js' - }, - plugins: [ - svelte({ - compilerOptions: { - // enable run-time checks when not in production - dev: !production - } - }), - // we'll extract any component CSS out into - // a separate file - better for performance - css({ output: 'bundle.css' }), + input: "src/main.ts", + output: { + sourcemap: true, + format: "iife", + name: "app", + file: "public/build/bundle.js", + }, + plugins: [ + svelte({ + preprocess: autoPreprocess(), + compilerOptions: { + // enable run-time checks when not in production + dev: !production, + }, + }), + typescript({ sourceMap: !production }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: "bundle.css" }), - // If you have external dependencies installed from - // npm, you'll most likely need these plugins. In - // some cases you'll need additional configuration - - // consult the documentation for details: - // https://github.com/rollup/plugins/tree/master/packages/commonjs - resolve({ - browser: true, - dedupe: ['svelte'] - }), - commonjs(), + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ["svelte"], + }), + commonjs(), - // In dev mode, call `npm run start` once - // the bundle has been generated - !production && serve(), + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), - // Watch the `public` directory and refresh the - // browser on changes when not in production - !production && livereload('public'), + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload("public"), - // If we're building for production (npm run build - // instead of npm run dev), minify - production && terser() - ], - watch: { - clearScreen: false - } + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser(), + ], + watch: { + clearScreen: false, + }, }; diff --git a/src/App.svelte b/src/App.svelte index 8418216..21b5cc3 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,76 +1,82 @@ -
- + {#if isSpectateOpen} -
isSpectateOpen = false} on:keydown={() => isSpectateOpen = false}> - -
+
(isSpectateOpen = false)} on:keydown={() => (isSpectateOpen = false)}> + +
{/if} {#if isFriendOpen} -
isFriendOpen = false} on:keydown={() => isFriendOpen = false}> - -
+
(isFriendOpen = false)} on:keydown={() => (isFriendOpen = false)}> + +
{/if} {#if isHistoryOpen} -
isHistoryOpen = false} on:keydown={() => isHistoryOpen = false}> - -
+
(isHistoryOpen = false)} on:keydown={() => (isHistoryOpen = false)}> + +
{/if} {#if isProfileOpen} -
isProfileOpen = false} on:keydown={() => isProfileOpen = false}> - -
+
(isProfileOpen = false)} on:keydown={() => (isProfileOpen = false)}> + +
{/if} +
\ No newline at end of file + diff --git a/src/components/Friends.svelte b/src/components/Friends.svelte index e9f1f85..452ec7b 100644 --- a/src/components/Friends.svelte +++ b/src/components/Friends.svelte @@ -1,69 +1,80 @@ - + +
-
-
- {#if friends.length > 0} -

Monkey friends

- {#each friends.slice(0, 10) as friends} -
  • - {friends.username} is {friends.status} -
  • - {/each} - {:else} -

    No friends to display

    - {/if} -
    -

    Add a friend

    -
    - - -
    -
    -
    -
    +
    +
    + {#if friends.length > 0} +

    Monkey friends

    + {#each friends.slice(0, 10) as friend} +
  • + {friend.username} is {friend.status} +
  • + {/each} + {:else} +

    No friends to display

    + {/if} +
    +

    Add a friend

    +
    + + +
    +
    +
    +
    - + \ No newline at end of file + .friends { + background-color: #fff; + border: 1px solid #ccc; + border-radius: 5px; + padding: 1rem; + width: 300px; + } + diff --git a/src/components/MatchHistory.svelte b/src/components/MatchHistory.svelte index 1c23d3e..68c9101 100644 --- a/src/components/MatchHistory.svelte +++ b/src/components/MatchHistory.svelte @@ -1,49 +1,58 @@ - + +
    -
    -
    - {#if matches.length > 0} -

    Last 10 monkey games

    - {#each matches.slice(0, 10) as match} -
  • - {match.winner} 1 - 0 {match.loser} - {#if match.points > 0} - +{match.points} - {:else} - {match.points} - {/if} - MP | rank #{match.rank} -
  • - {/each} - {:else} -

    No matches to display

    - {/if} -
    -
    +
    +
    + {#if matches.length > 0} +

    Last 10 monkey games

    + {#each matches.slice(0, 10) as match} +
  • + {match.winner} 1 - 0 {match.loser} + {#if match.points > 0} + +{match.points} + {:else} + {match.points} + {/if} + MP | rank #{match.rank} +
  • + {/each} + {:else} +

    No matches to display

    + {/if} +
    +
    - + \ No newline at end of file + .history { + background-color: #fff; + border: 1px solid #ccc; + border-radius: 5px; + padding: 1rem; + width: 300px; + } + diff --git a/src/components/NavBar.svelte b/src/components/NavBar.svelte index 0b6629d..a101a28 100644 --- a/src/components/NavBar.svelte +++ b/src/components/NavBar.svelte @@ -1,106 +1,106 @@ \ No newline at end of file + .navigation-bar li { + margin: 0; + padding: 1rem; + text-align: center; + } + } + diff --git a/src/components/Play.svelte b/src/components/Play.svelte index f777267..ca6f100 100644 --- a/src/components/Play.svelte +++ b/src/components/Play.svelte @@ -1,37 +1,38 @@ - +
    -

    Choose a gamemode

    - - - + +
    \ No newline at end of file + button { + font-size: 1.5rem; + padding: 1rem 2rem; + margin-bottom: 1rem; + } + diff --git a/src/components/Pong/Ball.ts b/src/components/Pong/Ball.ts new file mode 100644 index 0000000..cfb0828 --- /dev/null +++ b/src/components/Pong/Ball.ts @@ -0,0 +1,15 @@ +import { Point, Rect } from './utils'; + +export class Ball { + rect: Rect; + speed: Point; + color: string | CanvasGradient | CanvasPattern = 'white'; + + constructor(spawn: Point, size: Point = new Point(20, 20), speed: Point = new Point(10, 2)) { + this.rect = new Rect(spawn, size); + } + + draw(context: CanvasRenderingContext2D) { + this.rect.draw(context, this.color); + } +} diff --git a/src/components/Pong/Game.ts b/src/components/Pong/Game.ts new file mode 100644 index 0000000..8b37993 --- /dev/null +++ b/src/components/Pong/Game.ts @@ -0,0 +1,80 @@ +import { Ball } from './Ball'; +import { GAME_EVENTS } from './constants'; +import type { GameInfo, GameUpdate } from './constants'; +import { Paddle } from './Paddle'; +import { Player } from './Player'; +import { formatWebsocketData, Point } from './utils'; + +const BG_COLOR = 'black'; + +export class Game { + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + ball: Ball; + players: Player[]; + my_paddle: Paddle; + + constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) { + this.canvas = canvas; + this.context = context; + this.players = []; + this.my_paddle = null; + } + + setInfo(data: GameInfo) { + this.canvas.width = data.mapSize.x; + this.canvas.height = data.mapSize.y; + + this.ball = new Ball(new Point(this.canvas.width / 2, this.canvas.height / 2), data.ballSize); + + const paddle1: Paddle = new Paddle(new Point(data.playerXOffset, this.canvas.height / 2), data.paddleSize); + const paddle2: Paddle = new Paddle( + new Point(this.canvas.width - data.playerXOffset, this.canvas.height / 2), + data.paddleSize + ); + this.players = [new Player(paddle1), new Player(paddle2)]; + this.my_paddle = this.players[data.yourPaddleIndex].paddle; + } + + start(socket: WebSocket) { + if (this.my_paddle) { + this.canvas.addEventListener('mousemove', (e) => { + this.my_paddle.move(e); + socket.send( + formatWebsocketData(GAME_EVENTS.PLAYER_MOVE, { + position: this.my_paddle.rect.center + }) + ); + }); + console.log('Game started!'); + } + } + + update(data: GameUpdate) { + if (this.players[0].paddle != this.my_paddle) { + this.players[0].paddle.rect.center = data.paddlesPositions[0]; + } + if (this.players[1].paddle != this.my_paddle) { + this.players[1].paddle.rect.center = data.paddlesPositions[1]; + } + this.ball.rect.center = data.ballPosition; + for (let i = 0; i < data.scores.length; i++) { + this.players[i].score = data.scores[i]; + } + } + + draw() { + this.context.fillStyle = BG_COLOR; + this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + + this.players.forEach((p) => p.draw(this.context)); + this.ball.draw(this.context); + + const max_width = 50; + this.context.font = '50px Arial'; + const text_width = this.context.measureText('0').width; + const text_offset = 50; + this.players[0].drawScore(this.canvas.width / 2 - (text_width + text_offset), max_width, this.context); + this.players[1].drawScore(this.canvas.width / 2 + text_offset, max_width, this.context); + } +} diff --git a/src/components/Pong/Paddle.ts b/src/components/Pong/Paddle.ts new file mode 100644 index 0000000..ee7f419 --- /dev/null +++ b/src/components/Pong/Paddle.ts @@ -0,0 +1,29 @@ +import { Point, Rect } from './utils'; + +export class Paddle { + rect: Rect; + color: string | CanvasGradient | CanvasPattern = 'white'; + + constructor(spawn: Point, size: Point = new Point(6, 100)) { + this.rect = new Rect(spawn, size); + } + + draw(context: CanvasRenderingContext2D) { + this.rect.draw(context, this.color); + } + + move(e: MouseEvent) { + const canvas = e.target as HTMLCanvasElement; + const rect = canvas.getBoundingClientRect(); + const new_y = ((e.clientY - rect.top) * canvas.height) / rect.height; + + const offset: number = this.rect.size.y / 2; + if (new_y - offset < 0) { + this.rect.center.y = offset; + } else if (new_y + offset > canvas.height) { + this.rect.center.y = canvas.height - offset; + } else { + this.rect.center.y = new_y; + } + } +} diff --git a/src/components/Pong/Player.ts b/src/components/Pong/Player.ts new file mode 100644 index 0000000..eec3dc9 --- /dev/null +++ b/src/components/Pong/Player.ts @@ -0,0 +1,19 @@ +import type { Paddle } from './Paddle'; + +export class Player { + paddle: Paddle; + score: number; + + constructor(paddle: Paddle) { + this.paddle = paddle; + this.score = 0; + } + + draw(context: CanvasRenderingContext2D) { + this.paddle.draw(context); + } + + drawScore(score_position_x: number, max_width: number, context: CanvasRenderingContext2D) { + context.fillText(this.score.toString(), score_position_x, 50, max_width); + } +} diff --git a/src/components/Pong/Pong.svelte b/src/components/Pong/Pong.svelte new file mode 100644 index 0000000..43b2dbe --- /dev/null +++ b/src/components/Pong/Pong.svelte @@ -0,0 +1,65 @@ + + +
    + +
    +
    + +
    diff --git a/src/components/Pong/constants.ts b/src/components/Pong/constants.ts new file mode 100644 index 0000000..b06ad8d --- /dev/null +++ b/src/components/Pong/constants.ts @@ -0,0 +1,32 @@ +import { Point } from './utils'; + +export const GAME_EVENTS = { + START_GAME: 'START_GAME', + GAME_TICK: 'GAME_TICK', + PLAYER_MOVE: 'PLAYER_MOVE', + GET_GAME_INFO: 'GET_GAME_INFO' +}; + +export interface GameInfo extends GameInfoConstants { + yourPaddleIndex: number; +} +export interface GameInfoConstants { + mapSize: Point; + paddleSize: Point; + playerXOffset: number; + ballSize: Point; + winScore: number; +} +export const gameInfoConstants: GameInfoConstants = { + mapSize: new Point(600, 400), + paddleSize: new Point(6, 50), + playerXOffset: 50, + ballSize: new Point(20, 20), + winScore: 2 +}; + +export interface GameUpdate { + paddlesPositions: Point[]; + ballPosition: Point; + scores: number[]; +} diff --git a/src/components/Pong/pong.ts b/src/components/Pong/pong.ts new file mode 100644 index 0000000..bec771c --- /dev/null +++ b/src/components/Pong/pong.ts @@ -0,0 +1,52 @@ +import { GAME_EVENTS } from './constants'; +import { Game } from './Game'; +import { formatWebsocketData, Point } from './utils'; + +const FPS = 144; + +const socket: WebSocket = new WebSocket('ws://localhost:3001'); +socket.onopen = () => { + console.log('Connected to game server!'); + socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO)); +}; +let canvas: HTMLCanvasElement; +let context: CanvasRenderingContext2D; + +//Get canvas and its context +window.onload = () => { + document.getElementById('start_game_button').addEventListener('click', () => { + socket.send(formatWebsocketData(GAME_EVENTS.START_GAME)); + }); + canvas = document.getElementById('pong_canvas') as HTMLCanvasElement; + if (canvas) { + context = canvas.getContext('2d') as CanvasRenderingContext2D; + if (context) { + setupGame(); + } + } +}; + +function setupGame() { + const game = new Game(canvas, context); + + socket.onmessage = function (e) { + const event_json = JSON.parse(e.data); + const event = event_json.event; + const data = event_json.data; + + if (event == GAME_EVENTS.START_GAME) { + game.start(socket); + } else if (event == GAME_EVENTS.GAME_TICK) { + game.update(data); + } else if (event == GAME_EVENTS.GET_GAME_INFO) { + game.setInfo(data); + setInterval(() => { + game.draw(); + }, 1000 / FPS); + console.log('Game loaded!'); + } else { + console.log('Received unknown event from server:'); + console.log(event_json); + } + }; +} diff --git a/src/components/Pong/utils.ts b/src/components/Pong/utils.ts new file mode 100644 index 0000000..b083261 --- /dev/null +++ b/src/components/Pong/utils.ts @@ -0,0 +1,88 @@ +export class Point { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + //Returns a new point + add(other: Point) { + return new Point(this.x + other.x, this.y + other.y); + } + + //Modifies `this` point + add_inplace(other: Point) { + this.x += other.x; + this.y += other.y; + } + + clone(): Point { + return new Point(this.x, this.y); + } +} + +export class Rect { + center: Point; + size: Point; + + constructor(center: Point, size: Point) { + this.center = center; + this.size = size; + } + + draw(context: CanvasRenderingContext2D, color: string | CanvasGradient | CanvasPattern) { + const offset: Point = new Point(this.size.x / 2, this.size.y / 2); + + context.fillStyle = color; + context.fillRect(this.center.x - offset.x, this.center.y - offset.y, this.size.x, this.size.y); + } + + //True if `this` rect contains `other` rect in the x-axis + contains_x(other: Rect): boolean { + const offset: number = this.size.x / 2; + const offset_other: number = other.size.x / 2; + + if ( + this.center.x - offset <= other.center.x - offset_other && + this.center.x + offset >= other.center.x + offset_other + ) + return true; + return false; + } + + //True if `this` rect contains `other` rect in the y-axis + contains_y(other: Rect): boolean { + const offset: number = this.size.y / 2; + const offset_other: number = other.size.y / 2; + + if ( + this.center.y - offset <= other.center.y - offset_other && + this.center.y + offset >= other.center.y + offset_other + ) + return true; + return false; + } + + collides(other: Rect): boolean { + const offset: Point = new Point(this.size.x / 2, this.size.y / 2); + const offset_other: Point = new Point(other.size.x / 2, other.size.y / 2); + + if ( + this.center.x - offset.x < other.center.x + offset_other.x && + this.center.x + offset.x > other.center.x - offset_other.x && + this.center.y - offset.y < other.center.y + offset_other.y && + this.center.y + offset.y > other.center.y - offset_other.y + ) + return true; + return false; + } +} + +export function formatWebsocketData(event: string, data?: any): string { + return JSON.stringify({ + event, + data + }); +} diff --git a/src/components/Profile.svelte b/src/components/Profile.svelte index 6df0b59..210bb92 100644 --- a/src/components/Profile.svelte +++ b/src/components/Profile.svelte @@ -1,109 +1,109 @@ -
    -
    -
    - Profile Icon -

    {realname}

    -
    - -
    -
    -
    -
    -
    - - -
    - -
    -

    Wins: {wins}

    -

    Losses: {losses}

    -

    Winrate: {wins / (wins + losses) * 100}%

    -

    Elo : {elo}

    -

    Rank: {rank}

    -
    - -
    -
    -
    +
    +
    + Profile Icon +

    {realname}

    +
    + +
    +
    +
    +
    +
    + + +
    + +
    +

    Wins: {wins}

    +

    Losses: {losses}

    +

    Winrate: {(wins / (wins + losses)) * 100}%

    +

    Elo : {elo}

    +

    Rank: {rank}

    +
    + +
    +
    +
    - + \ No newline at end of file + .two-factor-auth { + margin-top: 1rem; + } + diff --git a/src/components/Spectate.svelte b/src/components/Spectate.svelte index 09a49af..a9a4b36 100644 --- a/src/components/Spectate.svelte +++ b/src/components/Spectate.svelte @@ -1,46 +1,53 @@ - + +
    -
    -
    - {#if spectate.length > 0} -

    Monkey spectating

    - {#each spectate.slice(0, 10) as spectate} -
  • - {spectate.player1} VS {spectate.player2} - -
  • - {/each} - {:else} -

    No matches to spectate

    - {/if} -
    -
    +
    +
    + {#if spectate.length > 0} +

    Monkey spectating

    + {#each spectate.slice(0, 10) as _spectate} +
  • + {_spectate.player1} VS {_spectate.player2} + +
  • + {/each} + {:else} +

    No matches to spectate

    + {/if} +
    +
    - - \ No newline at end of file + .spectate { + background-color: #fff; + border: 1px solid #ccc; + border-radius: 5px; + padding: 1rem; + width: 300px; + } + diff --git a/tsconfig.json b/tsconfig.json index 465f549..dd2e147 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,32 @@ { - "compilerOptions": { - "target": "es6", - "module": "es6", - "strict": true, - "noImplicitAny": true, - "jsx": "preserve", - "importHelpers": true, - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, - "baseUrl": ".", - "types": ["svelte"], - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "skipDefaultLibCheck": true, - "strictNullChecks": false, - "declaration": false, - "downlevelIteration": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules/*", "public/*"] + "compilerOptions": { + "target": "es6", + "module": "es6", + "strict": true, + "noImplicitAny": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "types": [ + "svelte" + ], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipDefaultLibCheck": true, + "strictNullChecks": false, + "declaration": false, + "downlevelIteration": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules/*", + "public/*" + ], + "extends": "@tsconfig/svelte/tsconfig.json" } \ No newline at end of file