vvandenb
2 years ago
18 changed files with 926 additions and 493 deletions
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
<script lang="ts"> |
|||
import { GAME_EVENTS } from './constants'; |
|||
import { Game } from './Game'; |
|||
import { formatWebsocketData } from './utils'; |
|||
|
|||
const FPS = 144; |
|||
const SERVER_URL = 'ws://localhost:3001'; |
|||
|
|||
const socket: WebSocket = new WebSocket(SERVER_URL); |
|||
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 = () => { |
|||
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); |
|||
} |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<button |
|||
on:click={() => { |
|||
socket.send(formatWebsocketData(GAME_EVENTS.START_GAME)); |
|||
}} |
|||
> |
|||
Start game |
|||
</button> |
|||
<br /> |
|||
<br /> |
|||
<canvas id="pong_canvas" /> |
|||
</div> |
@ -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[]; |
|||
} |
@ -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); |
|||
} |
|||
}; |
|||
} |
@ -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 |
|||
}); |
|||
} |
Loading…
Reference in new issue