local pong done, start multiplayer implementation

This commit is contained in:
LuckyLaszlo
2022-10-26 22:21:55 +02:00
parent a831b7954c
commit d7aa2b633b
14 changed files with 350 additions and 323 deletions

View File

@@ -1,4 +1,11 @@
#!/bin/bash #!/bin/bash
npx tsc npx tsc
mkdir -p www mkdir -p www
cp ./src/client/*.js ./src/client/*.html ./www/ cp ./src/client/*.html ./www/
mkdir -p www/js
cp ./src/client/*.js ./www/js/
mkdir -p www/js/class
cp ./src/client/class/*.js ./www/js/class/

View File

@@ -0,0 +1,36 @@
class GameArea {
keys: string[];
interval: number = 0;
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
constructor() {
this.keys = [];
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
/* ratio 5/3 (1.66) */
const ratio = 1.66666;
this.canvas.width = 1500;
this.canvas.height = this.canvas.width / ratio;
let container = document.getElementById("canvas-container");
if (container)
container.insertBefore(this.canvas, container.childNodes[0]);
}
addKey(key: string) {
key = key.toLowerCase();
var i = this.keys.indexOf(key);
if (i == -1)
this.keys.push(key);
}
deleteKey(key: string) {
key = key.toLowerCase();
var i = this.keys.indexOf(key);
if (i != -1)
this.keys.splice(i, 1);
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
export {GameArea}

View File

@@ -1,60 +1,6 @@
// type Vector = { import {Vector, VectorInteger} from "./Vector.js";
// x: number; import {Component, Moving} from "./interface.js";
// y: number;
// }
class Vector {
x: number;
y: number;
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
assign(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class VectorInteger extends Vector {
// PLACEHOLDER
// VectorInteger with set/get dont work (No draw on the screen). Why ?
}
/*
class VectorInteger {
// private _x: number = 0;
// private _y: number = 0;
// constructor(x: number = 0, y: number = 0) {
// this._x = x;
// this._y = y;
// }
// get x(): number {
// return this._x;
// }
// set x(v: number) {
// // this._x = Math.floor(v);
// this._x = v;
// }
// get y(): number {
// return this._y;
// }
// set y(v: number) {
// // this._y = Math.floor(v);
// this._y = v;
// }
}
*/
interface Component {
pos: VectorInteger;
color: string;
ctx: CanvasRenderingContext2D; // TODO: reference in place of global 'pong.ctx' call
update(): void;
clear(): void;
}
class Rectangle implements Component { class Rectangle implements Component {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
@@ -99,14 +45,6 @@ class Rectangle implements Component {
} }
} }
interface Moving {
dir: Vector;
speed: number;
move(): void;
}
class MovingRectangle extends Rectangle implements Moving { class MovingRectangle extends Rectangle implements Moving {
dir: Vector = new Vector(0,0); dir: Vector = new Vector(0,0);
speed: number; speed: number;
@@ -136,14 +74,12 @@ class MovingRectangle extends Rectangle implements Moving {
} }
} }
class Player extends MovingRectangle { class Player extends MovingRectangle {
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) { constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) {
super(ctx, pos, color, width, height, baseSpeed); super(ctx, pos, color, width, height, baseSpeed);
} }
} }
class Ball extends MovingRectangle { class Ball extends MovingRectangle {
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, baseSpeed: number) { constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, baseSpeed: number) {
super(ctx, pos, color, size, size, baseSpeed); super(ctx, pos, color, size, size, baseSpeed);
@@ -180,55 +116,6 @@ class Ball extends MovingRectangle {
} }
} }
// conflict with Text
class TextElem implements Component {
ctx: CanvasRenderingContext2D;
pos: VectorInteger;
color: string;
size: number;
font: string;
text: string = "";
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font: string = "Bit5x3") {
this.ctx = ctx;
this.pos = Object.assign({}, pos);
this.color = color;
this.size = size;
this.font = font;
}
update() {
this.ctx.font = this.size + "px" + " " + this.font;
this.ctx.fillStyle = this.color;
this.ctx.fillText(this.text, this.pos.x, this.pos.y);
}
clear() {
// clear no very accurate for Text
// https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics
let textMetric = this.ctx.measureText(this.text);
// console.log("textMetric.width = "+textMetric.width);
// console.log("size = "+this.size);
// console.log("x = "+this.pos.x);
// console.log("y = "+this.pos.y);
this.ctx.clearRect(this.pos.x - 1, this.pos.y-this.size + 1, textMetric.width, this.size);
// +1 and -1 because float imprecision (and Math.floor() with VectorInteger dont work for the moment)
// (or maybe its textMetric imprecision ?)
}
}
class TextNumericValue extends TextElem {
private _value: number = 0;
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font?: string) {
super(ctx, pos, color, size, font);
}
get value() {
return this._value;
}
set value(v: number) {
this._value = v;
this.text = v.toString();
}
}
class Line extends Rectangle { class Line extends Rectangle {
gapeCount: number = 0; gapeCount: number = 0;
segmentCount: number; segmentCount: number;
@@ -264,4 +151,4 @@ class Line extends Rectangle {
} }
} }
export {Vector, VectorInteger, Rectangle, MovingRectangle, Player, Ball, TextElem, TextNumericValue, Line} export {Rectangle, MovingRectangle, Player, Ball, Line}

53
src/client/class/Text.ts Normal file
View File

@@ -0,0 +1,53 @@
import {Vector, VectorInteger} from "./Vector.js";
import {Component, Moving} from "./interface.js";
// conflict with Text
class TextElem implements Component {
ctx: CanvasRenderingContext2D;
pos: VectorInteger;
color: string;
size: number;
font: string;
text: string = "";
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font: string = "Bit5x3") {
this.ctx = ctx;
this.pos = Object.assign({}, pos);
this.color = color;
this.size = size;
this.font = font;
}
update() {
this.ctx.font = this.size + "px" + " " + this.font;
this.ctx.fillStyle = this.color;
this.ctx.fillText(this.text, this.pos.x, this.pos.y);
}
clear() {
// clear no very accurate for Text
// https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics
let textMetric = this.ctx.measureText(this.text);
// console.log("textMetric.width = "+textMetric.width);
// console.log("size = "+this.size);
// console.log("x = "+this.pos.x);
// console.log("y = "+this.pos.y);
this.ctx.clearRect(this.pos.x - 1, this.pos.y-this.size + 1, textMetric.width, this.size);
// +1 and -1 because float imprecision (and Math.floor() with VectorInteger dont work for the moment)
// (or maybe its textMetric imprecision ?)
}
}
class TextNumericValue extends TextElem {
private _value: number = 0;
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font?: string) {
super(ctx, pos, color, size, font);
}
get value() {
return this._value;
}
set value(v: number) {
this._value = v;
this.text = v.toString();
}
}
export {TextElem, TextNumericValue}

View File

@@ -0,0 +1,45 @@
class Vector {
x: number;
y: number;
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
assign(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class VectorInteger extends Vector {
// PLACEHOLDER
// VectorInteger with set/get dont work (No draw on the screen). Why ?
}
/*
class VectorInteger {
// private _x: number = 0;
// private _y: number = 0;
// constructor(x: number = 0, y: number = 0) {
// this._x = x;
// this._y = y;
// }
// get x(): number {
// return this._x;
// }
// set x(v: number) {
// // this._x = Math.floor(v);
// this._x = v;
// }
// get y(): number {
// return this._y;
// }
// set y(v: number) {
// // this._y = Math.floor(v);
// this._y = v;
// }
}
*/
export {Vector, VectorInteger}

View File

@@ -0,0 +1,18 @@
import {Vector, VectorInteger} from "./Vector.js";
interface Component {
pos: VectorInteger;
color: string;
ctx: CanvasRenderingContext2D;
update(): void;
clear(): void;
}
interface Moving {
dir: Vector;
speed: number;
move(): void;
}
export {Component, Moving}

40
src/client/draw.ts Normal file
View File

@@ -0,0 +1,40 @@
import * as g from "./pong.js"
import {gridDisplay} from "./handleInput.js";
function draw()
{
if (gridDisplay) {
drawGrid();
}
g.midLine.update();
g.score1.update();
g.score2.update();
}
function drawStatic()
{
g.wall_top.update();
g.wall_bottom.update();
g.midLine.update();
}
function drawInit()
{
g.pong.clear();
drawStatic();
g.player1.update();
g.player2.update();
}
function drawGrid()
{
g.w_grid_mid.update();
g.w_grid_u1.update();
g.w_grid_d1.update();
g.h_grid_mid.update();
g.h_grid_u1.update();
g.h_grid_d1.update();
}
export {draw, drawStatic, drawInit, drawGrid}

60
src/client/gameLoop.ts Normal file
View File

@@ -0,0 +1,60 @@
import * as g from "./pong.js"
import * as d from "./draw.js";
import {random} from "./utils.js";
import {handleInput} from "./handleInput.js";
let ballInPlay = true;
function gameLoop()
{
/*
// I try to clear only what need to be update.
// Will revert to clear() all if not satisfactory.
pong.clear();
*/
handleInput();
if (ballInPlay)
{
g.ball.moveAndBounce([g.wall_top, g.wall_bottom, g.player1, g.player2]);
if (g.ball.pos.x > g.pong.canvas.width) {
ballInPlay = false;
g.score1.clear();
++g.score1.value;
setTimeout(newRound, 1500);
}
else if (g.ball.pos.x < 0 - g.ball.width) {
ballInPlay = false;
g.score2.clear();
++g.score2.value;
setTimeout(newRound, 1500);
}
}
d.draw();
}
function newRound()
{
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
if (g.score1.value >= 11
|| g.score2.value >= 11)
{
if (Math.abs(g.score1.value - g.score2.value) >= 2)
{
if (g.score1.value > g.score2.value) {
alert("Player 1 WIN");
}
else {
alert("Player 2 WIN");
}
return;
}
}
g.ball.pos.x = g.pong.canvas.width/2;
g.ball.pos.y = (g.pong.canvas.height * 0.1) + Math.floor(random() * (g.pong.canvas.height * 0.8));
g.ball.speed = g.ball.baseSpeed;
ballInPlay = true;
}
export {gameLoop}

46
src/client/handleInput.ts Normal file
View File

@@ -0,0 +1,46 @@
import * as g from "./pong.js"
import * as d from "./draw.js";
let gridDisplay = false;
function handleInput()
{
var keys = g.pong.keys;
if (keys.length == 0)
return;
if (keys.indexOf("g") != -1)
{
if (gridDisplay)
{
g.pong.clear();
d.drawStatic();
}
gridDisplay = !gridDisplay;
g.pong.deleteKey("g");
}
playerMove(keys);
}
function playerMove(keys: string[])
{
g.player1.dir.y = 0;
if (keys.indexOf("w") != -1) {
g.player1.dir.y += -1;
}
if (keys.indexOf("s") != -1) {
g.player1.dir.y += 1;
}
g.player1.moveAndCollide([g.wall_top, g.wall_bottom]);
g.player2.dir.y = 0;
if (keys.indexOf("ArrowUp".toLowerCase()) != -1) {
g.player2.dir.y += -1;
}
if (keys.indexOf("ArrowDown".toLowerCase()) != -1) {
g.player2.dir.y += 1;
}
g.player2.moveAndCollide([g.wall_top, g.wall_bottom]);
}
export {handleInput, gridDisplay}

View File

@@ -4,9 +4,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style> <style>
@font-face { @font-face {
font-family: 'Bit5x3'; font-family: "Bit5x3";
src: url('Bit5x3.woff2') format('woff2'), src: url("http://localhost:8080/Bit5x3.woff2") format("woff2"),
url('Bit5x3.woff') format('woff'); url("http://localhost:8080/Bit5x3.woff") format("woff");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -34,7 +34,7 @@
<div id="canvas-container"> <div id="canvas-container">
<!-- <p> =) </p> --> <!-- <p> =) </p> -->
</div> </div>
<script src="http://localhost:8080/pong.js" type="module" defer></script> <script src="http://localhost:8080/js/pong.js" type="module" defer></script>
</body> </body>
</html> </html>

View File

@@ -1,37 +1,38 @@
import {GameArea} from "./class/GameArea.js";
import {Vector, VectorInteger} from "./class/Vector.js";
import {Rectangle, MovingRectangle, Player, Ball, Line} from "./class/Rectangle.js";
import {TextElem, TextNumericValue} from "./class/Text.js";
import * as d from "./draw.js";
import {gameLoop} from "./gameLoop.js"
/* Keys /* Keys
Player 1: W/S Player 1: W/S
Player 2: Up/Down Player 2: Up/Down
Grid On-Off: G Grid On-Off: G
*/ */
import {Vector, VectorInteger, Rectangle, MovingRectangle, Player, Ball, TextElem, TextNumericValue, Line} from "./class.js"; export let pong: GameArea;
// @ts-check
let gridDisplay = false; export let wall_top: Rectangle;
let ballInPlay = true; export let wall_bottom: Rectangle;
export let player1: Player;
export let player2: Player;
export let ball: Ball;
export let score1: TextNumericValue;
export let score2: TextNumericValue;
export let midLine: Line;
let pong: gameArea; export let w_grid_mid: Rectangle;
export let w_grid_u1: Rectangle;
let wall_top: Rectangle; export let w_grid_d1: Rectangle;
let wall_bottom: Rectangle; export let h_grid_mid: Rectangle;
let player1: Player; export let h_grid_u1: Rectangle;
let player2: Player; export let h_grid_d1: Rectangle;
let ball: Ball;
let score1: TextNumericValue;
let score2: TextNumericValue;
let midLine: Line;
let w_grid_mid: Rectangle;
let w_grid_u1: Rectangle;
let w_grid_d1: Rectangle;
let h_grid_mid: Rectangle;
let h_grid_u1: Rectangle;
let h_grid_d1: Rectangle;
function startGame() function startGame()
{ {
pong = new gameArea(); pong = new GameArea();
// Const // Const
const w = pong.canvas.width; const w = pong.canvas.width;
@@ -90,187 +91,16 @@ function startGame()
// Start // Start
score1.update(); // first Text draw init the custom font (graphic leftover ortherwise) (a better solution ?) score1.update(); // first Text draw init the custom font (graphic leftover ortherwise) (a better solution ?)
drawInit(); d.drawInit();
pong.start(); window.addEventListener('keydown', function (e) {
pong.addKey(e.key);
});
window.addEventListener('keyup', function (e) {
pong.deleteKey(e.key);
});
pong.interval = window.setInterval(gameLoop, 20);
} }
class gameArea {
keys: string[];
interval: number = 0;
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
constructor() {
this.keys = [];
// this.canvas = {};
this.canvas = document.createElement("canvas");
// this.ctx = {};
this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
/* ratio 5/3 (1.66) */
const ratio = 1.66666;
this.canvas.width = 1500;
this.canvas.height = this.canvas.width / ratio;
let container = document.getElementById("canvas-container");
if (container)
container.insertBefore(this.canvas, container.childNodes[0]);
}
start() {
this.interval = window.setInterval(gameLoop, 20);
window.addEventListener('keydown', function (e) { pong.addKey(e.key); });
window.addEventListener('keyup', function (e) { pong.deleteKey(e.key); });
}
addKey(key: string) {
key = key.toLowerCase();
var i = pong.keys.indexOf(key);
if (i == -1)
pong.keys.push(key);
}
deleteKey(key: string) {
key = key.toLowerCase();
var i = pong.keys.indexOf(key);
if (i != -1)
pong.keys.splice(i, 1);
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
stop() {
clearInterval(this.interval);
}
}
function gameLoop()
{
/*
// I try to clear only what need to be update.
// Will revert to clear() all if not satisfactory.
pong.clear();
*/
handleInput();
if (ballInPlay)
{
ball.moveAndBounce([wall_top, wall_bottom, player1, player2]);
if (ball.pos.x > pong.canvas.width) {
ballInPlay = false;
score1.clear();
++score1.value;
setTimeout(newRound, 1500);
}
else if (ball.pos.x < 0 - ball.width) {
ballInPlay = false;
score2.clear();
++score2.value;
setTimeout(newRound, 1500);
}
}
draw();
}
function newRound()
{
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
if (score1.value >= 11
|| score2.value >= 11)
{
if (Math.abs(score1.value - score2.value) >= 2)
{
if (score1.value > score2.value) {
alert("Player 1 WIN");
}
else {
alert("Player 2 WIN");
}
return;
}
}
ball.pos.x = pong.canvas.width/2;
ball.pos.y = (pong.canvas.height * 0.1) + Math.floor(random() * (pong.canvas.height * 0.8));
ball.speed = ball.baseSpeed;
ballInPlay = true;
}
function random(min: number = 0, max: number = 1) {
return Math.random() * (max - min) + min;
}
function draw()
{
if (gridDisplay) {
drawGrid();
}
midLine.update();
score1.update();
score2.update();
}
function drawStatic()
{
wall_top.update();
wall_bottom.update();
midLine.update();
}
function drawInit()
{
pong.clear();
drawStatic();
player1.update();
player2.update();
}
function drawGrid()
{
w_grid_mid.update();
w_grid_u1.update();
w_grid_d1.update();
h_grid_mid.update();
h_grid_u1.update();
h_grid_d1.update();
}
function handleInput()
{
var keys = pong.keys;
if (keys.length == 0)
return;
if (keys.indexOf("g") != -1)
{
if (gridDisplay)
{
pong.clear();
drawStatic();
}
gridDisplay = !gridDisplay;
pong.deleteKey("g");
}
playerMove(keys);
}
function playerMove(keys: string[])
{
player1.dir.y = 0;
if (keys.indexOf("w") != -1) {
player1.dir.y += -1;
}
if (keys.indexOf("s") != -1) {
player1.dir.y += 1;
}
player1.moveAndCollide([wall_top, wall_bottom]);
player2.dir.y = 0;
if (keys.indexOf("ArrowUp".toLowerCase()) != -1) {
player2.dir.y += -1;
}
if (keys.indexOf("ArrowDown".toLowerCase()) != -1) {
player2.dir.y += 1;
}
player2.moveAndCollide([wall_top, wall_bottom]);
}
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////

6
src/client/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
function random(min: number = 0, max: number = 1) {
return Math.random() * (max - min) + min;
}
export {random}

View File

@@ -1,4 +1,3 @@
// @ts-check
// var http = require("http"); // var http = require("http");
// var url = require("url"); // var url = require("url");

BIN
www/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB