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 @@
-
-
-
-
-
-
Wins: {wins}
-
Losses: {losses}
-
Winrate: {wins / (wins + losses) * 100}%
-
Elo : {elo}
-
Rank: {rank}
-
-
-
+
+
+
+
+
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