main v2.0
Benjamin Kraft 2 years ago
commit 96d350d0be
  1. 4
      .gitignore
  2. 6
      project.json
  3. BIN
      public/data/images/background.jpg
  4. BIN
      public/data/images/border.jpg
  5. BIN
      public/data/images/border_icon_negative.png
  6. BIN
      public/data/images/border_icon_positive.png
  7. BIN
      public/data/images/favicon.ico
  8. BIN
      public/data/images/speed_icon_negative.png
  9. BIN
      public/data/images/speed_icon_positive.png
  10. 0
      public/data/scripts/ts/ball.ts
  11. 0
      public/data/scripts/ts/boost.ts
  12. 12052
      public/data/scripts/ts/definitions/p5/p5.d.ts
  13. 6480
      public/data/scripts/ts/definitions/p5/p5.global-mode.d.ts
  14. 88
      public/data/scripts/ts/definitions/serialized.d.ts
  15. 116
      public/data/scripts/ts/definitions/settings.d.ts
  16. 9
      public/data/scripts/ts/directions.ts
  17. 84
      public/data/scripts/ts/events.ts
  18. 367
      public/data/scripts/ts/game.ts
  19. 536
      public/data/scripts/ts/game_object.ts
  20. 168
      public/data/scripts/ts/init.ts
  21. 242
      public/data/scripts/ts/item.ts
  22. 37
      public/data/scripts/ts/loader.ts
  23. 27
      public/data/scripts/ts/lobby.ts
  24. 38
      public/data/scripts/ts/new_ball.ts
  25. 264
      public/data/scripts/ts/online.ts
  26. 401
      public/data/scripts/ts/player.ts
  27. 25
      public/data/scripts/ts/vector.ts
  28. 59
      public/data/scripts/ts/wormhole.ts
  29. 2
      public/data/settings/get_port.php
  30. 8
      public/data/settings/libraries.json
  31. 131
      public/data/settings/settings.json
  32. 88
      public/data/styles/color_picker.css
  33. BIN
      public/data/styles/font.ttf
  34. 88
      public/data/styles/range_input.css
  35. 72
      public/index.html
  36. 249
      public/package-lock.json
  37. 12
      public/package.json
  38. 272
      public/styles.css
  39. BIN
      public/thumbnail.png
  40. 9
      public/tsconfig.json
  41. 2
      server/.env
  42. 0
      server/logs/1679930598423.log
  43. 0
      server/logs/1679930618392.log
  44. 0
      server/logs/1679930648521.log
  45. 0
      server/logs/1679930712745.log
  46. 12
      server/logs/1679930782741.log
  47. 1247
      server/package-lock.json
  48. 18
      server/package.json
  49. 168
      server/src/client.ts
  50. 20
      server/src/definitions/serialized.d.ts
  51. 74
      server/src/definitions/settings.d.ts
  52. 37
      server/src/game_standard.ts
  53. 7
      server/src/index.ts
  54. 105
      server/src/logger.ts
  55. 148
      server/src/manager.ts
  56. 10
      server/src/pong.ts
  57. 135
      server/src/room.ts
  58. 39
      server/src/start.ts
  59. 11
      server/tsconfig.json

4
.gitignore vendored

@ -0,0 +1,4 @@
.idea
*.js
*.js.map
node_modules

@ -0,0 +1,6 @@
{
"display_name": "Pong remastered online",
"info_text": "My new version of the game Pong, also playable against friends on the internet",
"visible": true,
"tags": ["Game", "Multiplayer"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,88 @@
declare module Serialized{
interface GameObject{
id: string
}
interface Game extends GameObject{
players: Player[]
balls: Ball[]
boosts: Boost[]
wormholes: Wormhole[]
newBalls: NewBall[]
}
interface Lobby extends GameObject{
game: string
clientCounts: number[]
clients: Client[]
}
interface Client extends GameObject{
name: string
game: string
}
interface Ball extends GameObject{
pos: Vector
vel: Vector
color: Color
radius: number
runningUp: boolean
nextStep: Vector
}
interface Player extends GameObject{
pos: Vector
dim: Vector
vel: Vector
margin: number
moveMargin: number
color: Color
points: number
lobby: Lobby
input: Player.Input
boosts: ActiveBoost[]
frameRate: number
vision: boolean
}
interface Item extends GameObject{
pos: Vector
radius: number
color: Color
time: TimeProcess
currentRadius: number
}
interface Wormhole extends Item{
}
interface Boost extends Item{
type: string
bool: string
}
interface NewBall extends Item{
}
interface ActiveBoost extends GameObject{
type: string
bool: string
positive: boolean
time: TimeProcess
color: Color
}
interface Vector{
x: number
y: number
}
interface Color{
stroke: string
fill: string
}
interface TimeProcess{
max: number
now: number
finished: boolean
}
module Player{
interface Input{
up: boolean
down: boolean
left: boolean
right: boolean
}
}
}

@ -0,0 +1,116 @@
declare module Settings{
interface Global{
project: Project
frameWork: FrameWork
game: Game
}
interface Project{
name: string
author: string
playerCounts: number[]
online: Online
}
interface Online{
iceServers: ICEServer[]
}
interface ICEServer{
url: string
username: string
credential: string
}
interface FrameWork{
frameRate: number
updateRate: number
width: number
height: number
}
interface Game{
ball: Ball
player: Player
item: {
wormhole: Wormhole
boost: Boost
newBall: NewBall
}
}
interface Ball{
radius: number
velocity: Ball.Velocity
acceleration: Ball.Acceleration
runUp: Ball.RunUp
color: Color
safeRadius: number
}
interface Player{
length: number
depth: number
margin: number
points: number
absVel: number
color: Color
thisStroke: string
moveMargin: number
blinkTime: number
}
interface Item{
radius: number
spawnTime: number
color: Color
duration: number
fadeTime: number
}
interface Boost extends Item{
types: string[]
effect: {
[effect: string]: Effect
}
}
interface Wormhole extends Item{
power: number
minRadius: number
maxRadius: number
}
interface NewBall extends Item{
}
interface Color{
stroke: string
fill: string
}
interface Effect{
duration: number
positive: {
[key: string]: number
}
negative: {
[key: string]: number
}
standard : {
[key: string]: number
}
}
interface Vector{
x: number
y: number
}
interface TimeProcess{
max: number
now: number
}
module Ball{
interface RunUp{
min: number
max: number
}
interface Velocity{
start: number
min: number
runUpMax: number
resetMultiplier: number
}
interface Acceleration{
permanent: number
max: number
}
}
}

@ -0,0 +1,9 @@
enum Direction{
Top,
Right,
Bottom,
Left,
Forward,
Back,
Center
};

@ -0,0 +1,84 @@
function myKeyPressed() {
if ($('input').is(':focus')){
if (p.keyCode === 13 && $('#chat-input > input').is(':focus')){
onlineManager.sendMessage()
}
return true
}
if (game instanceof OnlineGame){
switch (p.keyCode){
case 38:
case 87:
game.input.up = true
break
case 40:
case 83:
game.input.down = true
break
case 65:
case 37:
game.input.left = true
break
case 68:
case 39:
game.input.right = true
break
}
game.sendInput()
}
switch (p.keyCode){
case 37:
case 38:
case 39:
case 40:
case 65:
case 68:
case 83:
case 87:
return false
}
}
function myKeyReleased() {
if ($('input').is(':focus')) return true
if (game instanceof OnlineGame){
switch (p.keyCode){
case 38:
case 87:
game.input.up = false
break
case 40:
case 83:
game.input.down = false
break
case 65:
case 37:
game.input.left = false
break
case 68:
case 39:
game.input.right = false
break
}
game.sendInput()
}
switch (p.keyCode){
case 32:
game.togglePlay(true, false)
return false
case 37:
case 38:
case 39:
case 40:
case 65:
case 68:
case 83:
case 87:
return false
}
}
window.onresize = () => {
adjustDOMSizes()
}

@ -0,0 +1,367 @@
class Game{
//Absolute Main class for everything that is happening in pong
//Contains everything that appears in the gameplay
//Update method applies frame updates to every entity and/or process
//Settings
settings: Settings.Game
//Entities
players: Player[]
balls: Ball[]
//Items
boosts: Boost[]
wormholes: Wormhole[]
newBalls: NewBall[]
//Gameplay
finished: boolean
paused: boolean
constructor(settings: Settings.Game){
this.settings = settings
}
init(lobby?: Lobby): void{
this.players = this.createPlayers(['0', '1'])
this.balls = this.createBalls('0')
this.boosts = []
this.wormholes = []
this.newBalls = []
this.play()
}
createPlayer(side: Direction, id: string, otherCount: number): Player{
return new Player(side, id, this.settings.player, otherCount)
}
createPlayers(ids: string[]): Player[]{
switch (ids.length){
case 2:
return [
this.createPlayer(Direction.Left, ids[0], 2),
this.createPlayer(Direction.Right, ids[1], 2)
]
case 3:
return [
this.createPlayer(Direction.Left, ids[0], 3),
this.createPlayer(Direction.Right, ids[1], 3),
this.createPlayer(Direction.Top, ids[2], 3)
]
case 4:
return [
this.createPlayer(Direction.Left, ids[0], 4),
this.createPlayer(Direction.Right, ids[1], 4),
this.createPlayer(Direction.Top, ids[2], 4),
this.createPlayer(Direction.Bottom, ids[3], 4)
]
}
}
createBall(id: string): Ball{
return new Ball(this.settings.ball, id)
}
createBalls(...ids: string[]): Ball[]{
let balls = []
for (let id of ids)
balls.push(this.createBall(id))
return balls
}
finish(): void{
this.pause()
this.finished = true
}
restart(): void{
this.finished = false
this.init()
}
play(): void{
if (this.finished) this.restart()
this.paused = false
$('#toggle-play').addClass('paused')
}
pause(): void{
if (this.finished) return
this.paused = true
$('#toggle-play').removeClass('paused')
}
togglePlay(fromClient?: boolean, fromInitiator?: boolean): void{
$('#toggle-play').blur()
if (this.paused){
this.play()
} else {
this.pause()
}
}
update(online?: boolean): void{
if (this.paused) return
for (let p of this.players) p.update(online)
for (let b of this.balls) b.update(this.balls, this.players, this.boosts, this.wormholes, this.newBalls)
for (let b of this.boosts) b.update()
for (let b of this.wormholes) b.update()
for (let n of this.newBalls) n.update()
this.tryCreateItem(Boost, this.settings.item.boost, this.boosts)
this.tryCreateItem(Wormhole, this.settings.item.wormhole, this.wormholes)
this.tryCreateItem(NewBall, this.settings.item.newBall, this.newBalls)
for (let p of this.players) if (p.hasLost) this.finish()
}
tryCreateItem(Type: typeof Item, settings: Settings.Item, items: Item[]){
if (ranBool(p.int(p.frameRate()) * settings.spawnTime)){
items.push(Item.fromNew(this.players, settings, Type))
}
}
display(): void{
this.deadZone()
for (let p of this.players) p.show()
for (let b of this.balls) b.show()
for (let b of this.boosts) b.show()
for (let w of this.wormholes) w.show()
for (let n of this.newBalls) n.show()
}
deadZone(): void{
p.noFill()
p.stroke(255, 0, 0)
p.strokeWeight(3)
for (let pl of this.players){
p.beginShape()
if (pl.isLeft || pl.isRight){
for (let y = pl.boxTopSide; y < pl.boxBottomSide; y++){
let x = p.map(p.noise(p.random(p.frameCount)), 0, 1, -pl.width / 2, pl.width / 2) + pl.centerX
p.vertex(x, y)
}
}
if (pl.isTop || pl.isBottom){
for (let x = pl.boxLeftSide; x < pl.boxRightSide; x++){
let y = p.map(p.noise(p.random(p.frameCount)), 0, 1, -pl.height / 2, pl.height / 2) + pl.centerY
p.vertex(x, y)
}
}
p.endShape()
}
}
}
class OnlineGame extends Game{
//Main class for a game that is played via WebRTC Connection
id: string
lobby: Lobby
socket: any
p2p: any
input: Serialized.Player.Input
initiator: boolean
packetManager: {
count: number
lastTime: number
deltaTime: number
time: number
}
constructor(socket: any, p2p: any, settings: Settings.Game){
super(settings)
this.socket = socket
this.p2p = p2p
this.packetManager = {
count: 0,
lastTime: Date.now(),
deltaTime: 0,
time: 0
}
}
set data(data: Serialized.Game){
let pm = this.packetManager
pm.count++
pm.deltaTime = Date.now() - pm.lastTime
pm.lastTime = Date.now()
pm.time += pm.deltaTime
if (pm.time > 5000){
console.log(pm.count + ' Data packs received from leading Peer, last one: ', data)
pm.count = 0
pm.time = 0
}
//Function to apply new values to objects which can disappear or appear
function applyValues(Type: typeof GameObject | typeof Player | typeof Ball | typeof Item,
objects1: Serialized.GameObject[], objects2: GameObject[],
settings: any): void{
for (let o1 of objects1){
let found = false
for (let o2 of objects2){
if (o1.id === o2.id){
o2.applyValues(o1)
found = true
}
}
if (!found){
let newObject
switch (Type){
case Boost:
case Wormhole:
newObject = Item.fromSerialized(o1, settings, Type)
break
case Ball:
newObject = Ball.fromSerialized(o1, settings)
break
case NewBall:
newObject = Item.fromSerialized(o1, settings, Type)
break
}
objects2.push(newObject)
}
}
for (let o2 of objects2){
let found = false
for (let o1 of objects1){
if (o1.id === o2.id) found = true
}
if (!found){
let index = objects2.indexOf(o2)
objects2.splice(index, 1)
}
}
}
applyValues(Player, data.players, this.players, this.settings.player)
applyValues(Ball, data.balls, this.balls, this.settings.ball)
applyValues(Boost, data.boosts, this.boosts, this.settings.item.boost)
applyValues(Wormhole, data.wormholes, this.wormholes, this.settings.item.wormhole)
applyValues(NewBall, data.newBalls, this.newBalls, this.settings.item.newBall)
}
init(lobby: Lobby): void{
this.input = {up: false, down: false, left: false, right: false}
this.lobby = lobby
this.id = this.lobby.id
//The initiator runs the actual game
//and sends the game to other peers
this.initiator = (this.socket.id === this.lobby.leader.id)
let ids = []
for (let p of this.lobby.clients) ids.push(p.id)
this.players = this.createPlayers(ids)
this.balls = this.createBalls(this.lobby.leader.id)
this.boosts = []
this.wormholes = []
this.newBalls = []
}
createPlayer(side: Direction, id: string, otherCount: number): Player{
return new Player(side, id, this.settings.player, otherCount, this.lobby)
}
setPlayerInput(player: any): void{
for (let p of this.players){
if (p.id === player.id){
p.input = player.input
}
}
}
sendInput(): void{
let player = {
id: this.getThisPlayer().id,
input: this.input,
lobby: this.getThisPlayer().lobby
}
if (this.initiator){
this.setPlayerInput(player)
} else {
this.p2p.emit('player-input', player)
}
}
getThisPlayer(): Player{
for (let p of this.players){
if (p.id === this.socket.id)
return p
}
}
togglePlay(fromClient: boolean, fromInitiator: boolean): void{
if (this.initiator){
super.togglePlay()
this.emitAction('togglePlay')
} else {
if (fromInitiator){
super.togglePlay()
} else {
if (fromClient){
this.emitAction('togglePlay')
}
}
}
}
update(): void{
if (this.initiator){
super.update(true)
this.p2p.emit('game-data', this.serialized())
}
}
emitAction(action: string){
this.p2p.emit('game-action', {
action: action,
lobby: this.lobby.serialized(),
fromInitiator: this.initiator
})
}
finish(): void{
super.finish()
if (this.initiator)
this.emitAction('finish')
}
restart(){
this.finished = false
this.init(this.lobby)
}
serialized(): Serialized.Game{
let players: Serialized.Player[] = [],
balls: Serialized.Ball[] = [],
boosts: Serialized.Boost[] = [],
wormholes: Serialized.Wormhole[] = [],
newBalls: Serialized.NewBall[] = []
for (let b of this.balls) balls.push(b.serialized())
for (let p of this.players) players.push(p.serialized())
for (let b of this.boosts) boosts.push(b.serialized())
for (let w of this.wormholes) wormholes.push(w.serialized())
for (let n of this.newBalls) newBalls.push(n.serialized())
return {
players: players,
balls: balls,
boosts: boosts,
wormholes: wormholes,
newBalls: newBalls,
id: this.id
}
}
}

@ -0,0 +1,536 @@
class GameObject{
id: string
constructor(id: string){
this.id = id
}
applyValues(state: Serialized.GameObject | {[key: string]: number}): void{
let recurse = (objIn: any, objOut: any): void => {
for (let p in objIn){
if (typeof objIn[p] == 'object' && objIn[p] != null){
if (!objOut[p]) objOut[p] = {};
recurse(objIn[p], objOut[p])
} else objOut[p] = objIn[p]
}
}
recurse(state, this)
}
}
class Ball extends GameObject{
//Main class for the moving ball object and the gameboard
//Settings
settings: Settings.Ball
//General
color: Settings.Color
runUp: RunUp
runningUp: boolean
timeAfterRunUp: number
lastPlayerHit: Player
collides: boolean
//Position and dimensions
pos: Vector
radius: number
//Movement
vel: Vector
acc: Vector
maxAcc: number
permaAcc: number
nextStep: Vector
constructor(settings: Settings.Ball, id: string){
super(id)
this.settings = settings
this.initSetup(true)
}
get canCollide(): boolean{
return this.dist(p.width / 2, p.height / 2) > this.settings.safeRadius
}
get future(): Ball{
let future = this.copy()
future.move()
return future
}
get nextPosition(): Vector{
let future = this.pos.copy()
future.add(this.vel)
return future
}
get downSide(): number{return this.pos.y + this.radius}
get upSide(): number{return this.pos.y - this.radius}
get leftSide(): number{return this.pos.x - this.radius}
get rightSide(): number{return this.pos.x + this.radius}
get centerX(): number{return this.pos.x}
get centerY(): number{return this.pos.y}
get size(): number{return this.radius * 2}
static fromSerialized(ball: Serialized.GameObject, settings: Settings.Ball): Ball{
let b = new Ball(settings, ball.id)
b.applyValues(ball)
return b
}
initSetup(hardReset: boolean): void{
let s = this.settings
this.setup(s.radius, s.velocity, s.acceleration, s.runUp, s.color, hardReset)
}
setup(r: number, v: Settings.Ball.Velocity, a: Settings.Ball.Acceleration, s: Settings.Ball.RunUp, c: Settings.Color, hardReset: boolean): void{
this.pos = new Vector(p.width / 2, p.height / 2)
this.acc = new Vector(0, 0)
this.maxAcc = a.max
this.permaAcc = a.permanent
if (hardReset) this.vel = new Vector(0, v.start)
else if (this.vel.mag() * v.resetMultiplier > v.min) this.vel.mult(v.resetMultiplier)
this.radius = r
this.color = c
this.runUp = new RunUp(s, v)
this.runningUp = true
}
reset(hardReset: boolean): void{
this.initSetup(hardReset)
}
update(balls: Ball[], players: Player[],
boosts: Boost[], wormholes: Wormhole[], newBalls: NewBall[]): void{
if (this.runningUp){
this.runUp.update(this)
} else {
let items = (boosts as Item[]).concat(newBalls)
this.collision(balls, players, items)
this.checkWormholes(wormholes)
this.move()
}
}
move(){
this.acc.limit(this.maxAcc)
this.vel.addMag(this.permaAcc)
this.vel.add(this.acc)
if (this.nextStep){
this.pos.add(this.nextStep)
this.nextStep = null
} else {
this.pos.add(this.vel)
}
this.acc.mult(0)
}
applyForce(force: Vector){
this.acc.add(force.div(this.radius))
}
checkWormholes(wormholes: Wormhole[]){
for (let w of wormholes){
w.attractBall(this)
}
}
collision(balls: Ball[], players: Player[], items: Item[]): void{
this.collides = false
if (this.itemCollision(items)) return
if (this.playerCollision(players)) return
if (this.deadZoneCollision(players)) return
if (this.wallCollision()) return
if (this.ballCollision(balls)) return
}
itemCollision(items: Item[]): boolean{
let item = this.touchesItems(items)
if (item){
//Add new Item interactions with ball collision here
if (item instanceof Boost && this.lastPlayerHit) this.lastPlayerHit.applyBoost(item as Boost)
if (item instanceof NewBall) game.balls.push(new Ball(this.settings, randomToken()))
item.destroy()
return true
}
return false
}
playerCollision(players: Player[]): boolean{
let player = this.touchesPlayers(players)
if (player){
this.turnXY(player)
this.lastPlayerHit = player
return true
}
return false
}
deadZoneCollision(players: Player[]): boolean{
for (let p of players){
if (this.willBeInDeadZone(p) || this.isInDeadZone(p)){
if (this.isInDeadZone(p)){
p.lost(this)
return true
} else {
this.moveIntoDeadZone(p)
}
}
}
return false
}
wallCollision(): boolean{
if (this.willTouchWall() || this.touchesWall()){
if (this.touchesWall()) {
this.turnY()
return true
} else {
this.moveOnWall()
}
}
return false
}
ballCollision(balls: Ball[]): boolean{
if (!this.canCollide) return false
for (let b of balls){
if (b.id === this.id || b.collides) continue
if (this.willTouchBall(b) || this.touchesBall(b)){
b.collides = true
if (this.touchesBall(b)){
this.performBallCollision(b)
return true
}
this.moveOnBall(b)
}
}
return false
}
touchesItems(items: Item[]): Item{
for (let i of items){
if (this.willTouchItem(i)){
if (this.touchesItem(i)){
return i
}
}
}
return null
}
touchesPlayers(players: Player[]): Player{
for (let p of players){
if (this.willTouchPlayer(p) || this.touchesPlayer(p)){
if (this.touchesPlayer(p)){
return p
} else {
this.moveOnPlayer(p)
}
}
}
return null
}
willTouchWall(): boolean{
return this.future.touchesWall()
}
touchesWall(): boolean{
return this.downSide >= p.height || this.upSide <= 0
}
posTouchesWall(pos: Vector): boolean{
let copy = this.copy()
copy.pos.x = pos.x
copy.pos.y = pos.y
return copy.downSide >= p.height || copy.upSide <= 0
}
moveOnWall(): void{
let pos = this.pos.copy()
let dir = new Vector(0, 0)
let vel = Vector.fromAngle(this.vel.heading()).mult(0.5)
while (!this.posTouchesWall(pos)){
pos.add(vel)
dir.add(vel)
if (dir.mag() > this.vel.mag())
break //error
}
if (!this.nextStep) this.nextStep = dir
}
willTouchItem(item: Item): boolean{
let now = this.copy()
let then = this.future
let dist = Vector.sub(then.pos, now.pos).mag()
let nowDist = 0
while(nowDist <= dist){
if (now.touchesItem(item)){
return true
}
now.pos.addMag(1)
nowDist++
}
return false
}
touchesItem(item: Item): boolean{
let dist = this.dist(item.pos.x, item.pos.y)
return dist <= this.radius + item.radius
}
willTouchPlayer(player: Player): boolean{
return this.future.touchesPlayer(player.future)
}
touchesPlayer(player: Player): boolean{
if (player.isLeft || player.isRight){
for (let y = player.upSide; y <= player.downSide; y++){
let touch = this.touchesPos(player.toGameSide, y)
if (touch) return true
}
}
if (player.isTop || player.isBottom){
for (let x = player.leftSide; x <= player.rightSide; x++){
let touch = this.touchesPos(x, player.toGameSide)
if (touch) return true
}
}
return false
}
posTouchesPlayer(pos: Vector, player: Player): boolean{
if (player.isLeft || player.isRight){
for (let y = player.upSide; y <= player.downSide; y++){
if (p.dist(pos.x, pos.y, player.toGameSide, y) - this.radius <= 0.2) return true
}
}
if (player.isTop || player.isBottom){
for (let x = player.leftSide; x <= player.rightSide; x++){
if (p.dist(pos.x, pos.y, x, player.toGameSide) - this.radius <= 0.2) return true
}
}
return false
}
moveOnPlayer(player: Player): void{
let pos = this.pos.copy()
let dir = new Vector(0, 0)
let vel = Vector.fromAngle(this.vel.heading()).mult(0.5)
while (!this.posTouchesPlayer(pos, player)){
pos.add(vel)
dir.add(vel)
if (dir.mag() > this.vel.mag())
break //error
}
this.nextStep = dir
}
isInDeadZone(player: Player): boolean{
if ((this.centerX < player.leftSide && player.isLeft ||
this.centerX > player.rightSide && player.isRight)
&& this.centerY > player.boxTopSide
&& this.centerY < player.boxBottomSide){
return true
}
if ((this.centerY < player.upSide && player.isTop ||
this.centerY > player.downSide && player.isBottom)
&& this.centerX > player.boxLeftSide
&& this.centerX < player.boxRightSide){
return true
}
}
posIsInDeadZone(pos: Vector, player: Player): boolean{
let copy = this.copy()
copy.pos = pos.copy()
if ((copy.centerX < player.leftSide && player.isLeft ||
copy.centerX > player.rightSide && player.isRight)
&& copy.centerY > player.boxTopSide
&& copy.centerY < player.boxBottomSide){
return true
}
if ((copy.centerY < player.upSide && player.isTop ||
copy.centerY > player.downSide && player.isBottom)
&& copy.centerX > player.boxLeftSide
&& copy.centerX < player.boxRightSide){
return true
}
}
willBeInDeadZone(player: Player): boolean{
return this.posIsInDeadZone(this.future.pos.copy(), player)
}
moveIntoDeadZone(player: Player): void{
let pos = this.pos.copy()
let dir = new Vector(0, 0)
let vel = Vector.fromAngle(this.vel.heading()).mult(0.5)
while (!this.posIsInDeadZone(pos, player)){
pos.add(vel)
dir.add(vel)
if (dir.mag() > this.vel.mag())
break //error
}
this.nextStep = dir
}
touchesBall(ball: Ball): boolean{
return this.dist(ball.centerX, ball.centerY) <= this.radius + ball.radius
}
willTouchBall(ball: Ball): boolean{
return this.future.touchesBall(ball.future)
}
moveOnBall(ball: Ball): void{
let pos = this.pos.copy()
let dir = new Vector(0, 0)
let vel = Vector.fromAngle(this.vel.heading()).mult(0.5)
while (!this.posTouchesBall(pos, ball)){
pos.add(vel)
dir.add(vel)
if (dir.mag() > this.vel.mag())
break //error
}
this.nextStep = dir
}
posTouchesBall(pos: Vector, ball: Ball): boolean{
let b = this.copy()
b.pos = pos.copy()
return b.touchesBall(ball)
}
performBallCollision(ball: Ball): void{
Collision.ellipseToEllipse(this, ball)
}
touchesPos(x: number, y: number): boolean{
let d = this.dist(x, y)
return d <= this.radius + 0.2
}
dist(x: number, y: number): number{
return p.dist(x, y, this.centerX, this.centerY)
}
turnY(): void{
this.vel.y *= -1
}
turnX(): void{
this.vel.x *= -1
}
turnXY(player: Player): void{
if (player.isLeft || player.isRight){
this.turnX()
// Number between -0.5 (ball on the top) and 0.5 (ball on the bottom of player)
let divider = (this.centerY - (player.upSide - this.radius)) / (player.height + this.radius * 2) - 0.5
this.vel.rotate((player.isLeft ? 1 : -1) * divider)
}
if (player.isTop || player.isBottom){
this.turnY()
// Number between -0.5 (ball on the left) and 0.5 (ball on the right of player)
let divider = (this.centerX - (player.leftSide - this.radius)) / (player.width + this.radius * 2) - 0.5
this.vel.rotate((player.isBottom ? 1 : -1) * divider)
}
}
copy(): Ball{
let ball = new Ball(this.settings, this.id)
ball.pos = this.pos.copy()
ball.vel = this.vel.copy()
ball.nextStep = this.nextStep
ball.radius = this.radius
return ball
}
serialized(): Serialized.Ball{
return {
pos: this.pos.serialized(),
vel: this.vel.serialized(),
color: this.color,
radius: this.radius,
runningUp: this.runningUp,
nextStep: this.nextStep,
id: this.id
}
}
show(): void{
p.push()
p.fill(this.color.fill)
p.stroke(this.color.stroke)
p.translate(this.centerX, this.centerY)
p.strokeWeight(3)
p.ellipse(0, 0, this.size, this.size)
if (this.runningUp){
p.stroke(0, 255, 0)
p.fill(0, 100, 0)
p.rotate(this.vel.heading())
let mag = this.vel.mag() * 15
p.strokeWeight(mag / 50)
p.line(0, 0, mag, 0)
p.triangle(mag, -mag / 8, mag, mag / 8, mag + mag / 4, 0)
}
p.pop()
}
}
class RunUp{
time: TimeProcess
rVel = 0
mVel = 0
rAcc = 0
mAcc = 0
minVel: number
maxVel: number
constructor(s: Settings.Ball.RunUp, v: Settings.Ball.Velocity){
this.time = new TimeProcess(Math.random() * (s.max - s.min) + s.min)
this.minVel = v.min
this.maxVel = v.runUpMax
}
update(b: Ball): void{
this.time.update()
this.mAcc = (Math.random() - 0.5) / 50
this.rAcc = (Math.random() - 0.5) / 50
if (Math.abs(this.mVel) < 0.5)
this.mVel += this.mAcc
if (Math.abs(this.rVel) < 0.1)
this.rVel += this.rAcc
if (b.vel.mag() < this.maxVel && b.vel.mag() > this.minVel)
b.vel.addMag(this.mVel)
b.vel.rotate(this.rVel)
b.runningUp = !this.time.finished
if (!b.runningUp){
b.lastPlayerHit = null
}
}
}

@ -0,0 +1,168 @@
let debug = false,
viewPort = {x: 0, y: 0},
localFont: any,
localSettings: Settings.Global;
let state = 'menu',
game: Game | OnlineGame,
onlineManager: OnlineManager,
loader: Loader,
icons : {
speed: {
positive
negative
}
border: {
positive
negative
}
}
const p = new p5((p: p5) => {
p.preload = (): void => {
localSettings = <Settings.Global>p.loadJSON('data/settings/settings.json', {}, "json", (json) => {
console.log('Local settings loaded: ', json)
}, (error) => {
console.log('Local settings failed: ', error)
})
localFont = p.loadFont('data/styles/font.ttf', (json) => {
console.log('Local font loaded: ', json)
}, error => {
console.log('Local font failed: ', error)
})
p.loadJSON('data/settings/libraries.json', json => {
loadScripts(json)
console.log('BenjoCraeft library scripts loaded: ', json)
})
icons = {speed: {positive: null, negative: null}, border: {positive: null, negative: null}}
icons.speed.positive = p.loadImage('data/images/speed_icon_positive.png')
icons.speed.negative = p.loadImage('data/images/speed_icon_negative.png')
icons.border.positive = p.loadImage('data/images/border_icon_positive.png')
icons.border.negative = p.loadImage('data/images/border_icon_negative.png')
}
p.setup = (): void => {
canvasSetup(localSettings.frameWork, localFont)
interfaceSetup()
}
p.draw = (): void => {
p.background(90, 90, 120)
//p.image(img, 0, 0, p.width, p.height)
if (game){
game.update()
game.display()
}
if (loader){
loader.update()
loader.display()
}
if (debug) debugInformation()
}
});
p.keyPressed = myKeyPressed;
p.keyReleased = myKeyReleased;
declare let debugInformation: () => void
declare let ranBool: (chance: number) => boolean
declare let Collision: {
ellipseToEllipse: (e1: Ball, e2: Ball) => void
}
function canvasSetup(settings: Settings.FrameWork, font): void{
p.frameRate(settings.frameRate)
let canvas = p.createCanvas(settings.width, settings.height)
canvas.parent('canvasHolder')
adjustDOMSizes()
p.textFont(font)
}
function startOfflineGame(): void{
$('#setup-controls').hide()
$('#game-controls').show()
game = new Game(localSettings.game)
game.init()
console.log('Offline Game started:', game)
}
function adjustDOMSizes(){
let w = window.innerWidth
let h = window.innerHeight
$('canvas, #canvasHolder').css({
width: h * 0.95,
height: h * 0.95
})
$('#interface, #chat').css({
width: (w - h * 0.95 - 4) / 2 - 4 - 10,
height: h * 0.95,
'margin-top': h * 0.025 - 2,
'margin-left': 5
})
$('#chat').css({
'margin-right': 5
})
}
function interfaceSetup(): void{
function handleRadioInput(): void{
let type = $('#game-type input[type=radio]:checked').val()
$('#game-properties > *').hide()
if (type === 'offline'){
$('.start').show()
state = 'offline-start'
} else if (type === 'online'){
$('.player-name').show()
state = 'player-name'
}
$('#game-properties').show()
}
$('#lobby, #chat').hide()
$('#game-type input[type=radio]').change(handleRadioInput)
handleRadioInput()
}
function continueForm(dom: any): void{
$(dom).blur()
switch(state){
case 'offline-start':
startOfflineGame()
break;
case 'player-name':
let input = getValidInput(state)
if (input){
onlineManager = new OnlineManager(input, localSettings)
$('.player-name').hide()
$('.player-actions').show()
state = 'player-actions'
}
break
}
}
function randomToken(): string{
return Math.floor((1 + Math.random()) * 1e16).toString(16).substring(1)
}
function loadScripts(libs: string[]){
for (let script in libs){
if (libs[script]){
let url = '/lib/benjocraeft/' + script + '.js'
$.getScript(url, () => {
console.log('Successfully loaded script: ', url)
})
}
}
}

@ -0,0 +1,242 @@
class Item extends GameObject{
//Superior class for everything apperaring on the gameboard
//after a period of time, for a period of time
//Settings
settings: Settings.Item
//General
color: Serialized.Color
time: TimeProcess
fadeTime: number
//Position and dimensions
pos: Vector
radius: number
currentRadius: number
constructor(settings: Settings.Item){
super(randomToken())
this.radius = settings.radius
this.fadeTime = settings.fadeTime
this.currentRadius = 1
this.color = {
stroke: settings.color.stroke,
fill: settings.color.fill
}
this.time = new TimeProcess(settings.duration)
}
get centerX(): number{return this.pos.x}
get centerY(): number{return this.pos.y}
get size(): number{return this.currentRadius * 2}
static fromSerialized(item: Serialized.GameObject, settings: Settings.Item, Type: typeof Item): Item{
let i = new Type(settings)
i.pos = new Vector(0, 0)
i.applyValues(item)
return i
}
static fromNew(players: Player[], settings: Settings.Item, Type: typeof Item): Item{
let i = new Type(settings)
i.pos = i.getStartPosition(players)
return i
}
getStartPosition(players: Player[]): Vector{
let left: number,
right: number,
top = this.radius,
bottom = p.height - this.radius
for (let p of players){
if (p.isLeft) left = p.rightSide + this.radius
if (p.isRight) right = p.leftSide - this.radius
if (p.isTop) top = p.downSide + this.radius
if (p.isBottom) bottom = p.upSide - this.radius
}
let x = p.random(left, right),
y = p.random(top, bottom)
return new Vector(x, y)
}
update(): void{
this.time.update()
if (this.time.finished) this.destroy()
if (this.time.timeLeft() < this.fadeTime){
this.currentRadius = p.pow(this.time.timeLeft() / this.fadeTime, 2) * this.radius
} else if (this.time.now < this.fadeTime){
this.currentRadius = p.pow(this.time.now / this.fadeTime, 2) * this.radius
}
}
destroy(): void{}
serialized(): Serialized.Item{
return {
id: this.id,
radius: this.radius,
pos: this.pos.serialized(),
color: this.color,
time: this.time,
currentRadius: this.currentRadius
}
}
}
class TimeProcess{
//Numbers in Milliseconds
max: number
now: number
finished: boolean
constructor(max){
this.max = max
this.now = 0
this.finished = false
}
update(): void{
this.now += 1000 / p.frameRate()
if (this.now >= this.max) this.finished = true
}
progress(): number{
return this.now / this.max
}
timeLeft(): number{
return this.max - this.now
}
serialized(): Serialized.TimeProcess{
return {
max: this.max,
now: this.now,
finished: this.finished
}
}
}
class Boost extends Item{
//Class for the boost objects appearing on the game board
//Inherits following properties from Item:
/*
id: string
pos: Vector
radius: number
color: Serialized.Color
settings: Settings.Item
time: TimeProcess
*/
type: string
positive: boolean
bool: string
effect: Settings.Effect
constructor(settings: Settings.Boost){
super(settings)
this.type = p.random(settings.types)
this.positive = ranBool(2)
this.bool = this.positive ? "positive" : "negative"
this.effect = settings.effect[this.type]
this.color = {
stroke: settings.color[this.bool].stroke,
fill: settings.color[this.bool].fill
}
}
destroy(): void{
let index = game.boosts.indexOf(this)
game.boosts.splice(index, 1)
}
serialized(): Serialized.Boost{
let item = super.serialized() as Serialized.Boost
item.type = this.type
item.bool = this.bool
return item
}
show(): void{
let angle = p.TWO_PI * (1 - this.time.now / this.time.max),
size = this.size
p.push()
p.translate(this.centerX, this.centerY)
p.image(icons[this.type][this.bool], -size / 2, -size / 2, size, size)
p.pop()
}
}
class ActiveBoost{
time: TimeProcess
type: string
positive: boolean
bool: string
player: Player
color: Settings.Color
effect: {[key: string]: number}
reset: {[key: string]: number}
id: string
constructor(player: Player, boost: Boost){
this.id = randomToken()
this.time = new TimeProcess(boost.effect.duration)
this.type = boost.type
this.color = {
stroke: boost.color.stroke,
fill: boost.color.fill
}
this.positive = boost.positive
this.bool = boost.bool
this.player = player
if (boost.positive) this.effect = boost.effect.positive
if (!boost.positive) this.effect = boost.effect.negative
this.reset = boost.effect.standard
}
serialized(): Serialized.ActiveBoost{
return {
time: this.time.serialized(),
type: this.type,
bool: this.bool,
positive: this.positive,
color: this.color,
id: this.id
}
}
update(): void{
this.player.applyValues(this.effect)
this.time.update()
if (this.time.finished){
this.player.applyValues(this.reset)
for (let b of this.player.boosts){
if (b.id === this.id){
let index = this.player.boosts.indexOf(b)
this.player.boosts.splice(index, 1)
break
}
}
}
}
}

@ -0,0 +1,37 @@
class Loader{
dim: p5.Vector;
c: any;
radius: number;
center: p5.Vector;
angle: number;
constructor(dom: any){
this.dim = p.createVector($(dom).width(), $(dom).height());
this.c = p.createGraphics(this.dim.x, this.dim.y, 'p2d');
this.c.parent(dom);
this.radius = p.min([this.dim.x, this.dim.y]) * 0.4;
this.center = p.createVector(this.dim.x / 2, this.dim.y / 2);
$(dom).find('canvas').show();
this.angle = 0;
$(dom).show();
}
update(): void{
this.angle += p.PI / 10;
}
display(): void{
let c = this.c;
c.clear();
c.noFill();
c.stroke(0);
c.strokeWeight(5);
c.arc(this.center.x, this.center.y, this.radius * 2, this.radius * 2, this.angle, this.angle + p.PI + p.HALF_PI);
}
destroy(): void{
this.c.remove()
}
}

@ -0,0 +1,27 @@
class Lobby implements Serialized.Lobby{
id: string
clients: Serialized.Client[]
clientCounts: number[]
game: string
constructor(lobby: Serialized.Lobby){
this.id = lobby.id
this.clients = lobby.clients
this.clientCounts = lobby.clientCounts
this.game = lobby.game
}
get leader(): Serialized.Client{
return this.clients[0]
}
serialized(): Serialized.Lobby{
return {
id: this.id,
game: this.game,
clientCounts: this.clientCounts,
clients: this.clients
}
}
}

@ -0,0 +1,38 @@
class NewBall extends Item{
//Class for the new ball objects appearing on the game board
//Inherits following properties from Item:
/*
id: string
pos: Vector
radius: number
color: Serialized.Color
settings: Settings.Item
time: TimeProcess
*/
constructor(settings: Settings.NewBall){
super(settings)
}
destroy(): void{
let index = game.newBalls.indexOf(this)
game.newBalls.splice(index, 1)
}
serialized(): Serialized.NewBall{
let newBall = super.serialized() as Serialized.NewBall
return newBall
}
show(): void{
p.push()
p.translate(this.centerX, this.centerY)
p.fill(this.color.fill)
p.stroke(this.color.stroke)
p.ellipse(0, 0, this.size, this.size)
p.pop()
}
}

@ -0,0 +1,264 @@
declare let P2P: any
declare let io: any
class OnlineManager{
socket: any
settings: Settings.Global
lobby: Lobby
p2p: any
constructor(name: string, settings: Settings.Global){
this.connectToServer(settings.project, name, () => {
this.connectToPeers(settings.project)
this.setSocketEvents()
this.settings = settings
});
}
connectToServer(project: Settings.Project, name: string, cb): void{
let urlQueries = '?game=' + project.name + '&name=' + name;
$.get('data/settings/get_port.php', port => {
let url = 'https://' + location.hostname + ':' + port + urlQueries
this.socket = io.connect(url)
this.socket.on('connect', () =>
console.log('Connected to ', url));
cb();
});
}
connectToPeers(project: Settings.Project){
let iceServers = project.online.iceServers
let opts = {peerOpts: {trickle: false, config: {iceServers: iceServers}}}
this.p2p = new P2P(this.socket, opts)
this.p2p.usePeerConnection = true
this.p2p.useSockets = false
console.log('Created WebRTC Connection:', this.p2p)
}
setSocketEvents(): void{
this.socket.on('connected', () =>
onlineAnswerFrontend())
this.socket.on('member-joined', (lobby: Serialized.Lobby) =>
this.setLobby(new Lobby(lobby)))
this.socket.on('member-left', (lobby: Serialized.Lobby) =>
this.setLobby(new Lobby(lobby)))
this.socket.on('join-failed', (error: string) =>
this.joinFailed(error))
this.socket.on('start-game', () =>
this.startOnlineGame())
}
setPeerEvents(){
//Game actions like togglePlay
this.p2p.on('game-action', (req: any) => {
if (req.lobby.id !== this.lobby.id) return
game[req.action](false, req.fromInitiator)
})
//For every non-leader, whole game sent by leader
this.p2p.on('game-data', (data: Serialized.Game) => {
if (data.id !== this.lobby.id) return
(game as OnlineGame).data = data
})
//For leader, inputs sent by others
this.p2p.on('player-input', (player: Serialized.Player) => {
if (player.lobby.id !== this.lobby.id) return
(game as OnlineGame).setPlayerInput(player)
})
this.p2p.on('chat-msg', (req: any) => {
if (req.lobby.id !== this.lobby.id) return
this.addChatMessage(req.msg)
})
this.p2p.on('peer-error', (data: any) =>
console.log('Peer-Error: ', data))
}
setLobby(lobby: Lobby): void{
if (!this.lobby) this.setPeerEvents()
this.lobby = lobby
onlineAnswerFrontend()
$('.error-label').html('')
$('#setup-controls').hide()
$('#lobby-members').html('')
this.setLobbyMembers()
this.setLeaderAbilites()
this.checkPlayerCount()
$('#lobby > span:eq(1)').html(this.lobby.id)
$('#lobby, #chat').show()
console.log('Set lobby with id: ', this.lobby.id)
}
setLobbyMembers(): void{
for (let c of this.lobby.clients){
let dom = $('<div></div>')
dom.attr('class', 'lobby-member border-node')
if (c.id === this.socket.id){
dom.attr('id', 'this-player')
}
if (this.lobby.leader.id === c.id){
dom.attr('id', 'lobby-leader')
}
dom.html(c.name)
$('#lobby-members').append(dom)
}
console.log('Set lobby members: ', this.lobby.clients)
}
setLeaderAbilites(): void{
if (this.socket.id === this.lobby.leader.id){
$('#start-lobby').show()
console.log('Leader: ', true)
} else {
$('#start-lobby').hide()
console.log('Leader: ', false)
}
}
checkPlayerCount(): void{
let wrongCount = true
let lo = this.lobby
let pc = lo.clientCounts
for (let c of pc){
if (c === lo.clients.length) wrongCount = false
}
if (wrongCount){
let error = 'Only as '
for (let c of pc){
let comma = pc.indexOf(c) === pc.length - 1 ? '' : ', '
error += c + comma
}
$('#lobby .error-label').html(error)
$('#start-lobby').prop('disabled', true)
} else {
$('#lobby .error-label').html('')
$('#start-lobby').prop('disabled', false)
}
}
createLobby(dom: any): void{
onlineRequestFrontend(dom)
this.socket.emit('create-lobby', this.settings)
console.log('Server request: create lobby')
}
joinLobby(dom: any): void{
let input = getValidInput('join')
if (input){
onlineRequestFrontend(dom)
this.socket.emit('join-lobby', input)
console.log('Server request: join lobby: ', input)
}
}
joinFailed(error: string): void{
onlineAnswerFrontend()
$('.error-label').html(error)
console.log('Joining lobby failed. Reason: ', error)
}
startLobby(dom: any): void{
onlineRequestFrontend(dom)
this.socket.emit('start-game', this.lobby.id, this.settings)
console.log('Server request: start lobby: ', this.lobby.id)
}
startOnlineGame(): void{
onlineAnswerFrontend()
$('#lobby').hide()
$('#game-controls').show()
game = new OnlineGame(this.socket, this.p2p, this.settings.game)
game.init(this.lobby)
console.log('Online Game started:', game)
}
addChatMessage(msg: any): void{
let chat = $('#chat-content'),
name: string,
message = $('<div></div>')
if (chat.find('.message').last().attr('name') === "0"){
name = "1"
} else name = "0"
message.addClass('message')
message.attr('name', name)
let writer = $('<span></span>'),
content = $('<span></span>')
writer.html('[' + msg.writer + ']: ')
content.html(msg.content)
message.append(writer, content)
chat.append(message)
message.get(0).scrollIntoView(false)
}
sendMessage(): void{
let content = $($('#chat-input > input')[0]).val(),
name: string
if (content === '')
return
$($('#chat-input > input')[0]).val('')
for (let c of this.lobby.clients){
if (c.id === this.socket.id)
name = c.name
}
let msg = {writer: name, content: content}
this.addChatMessage(msg)
this.p2p.emit('chat-msg', {
lobby: this.lobby.serialized(),
msg: {
writer: name,
content: content
}
})
}
}
function onlineRequestFrontend(dom: any): void{
$(dom).blur()
$('.setup').prop('disabled', true)
if (loader) loader.destroy()
loader = new Loader($('#loader').get(0))
}
function onlineAnswerFrontend(): void{
$('.setup').prop('disabled', false)
if (loader) loader.destroy()
}
function getValidInput(type: string): string{
$('.error-label').html('')
if (type === 'player-name'){
let val = $('#player-name > input').val()
if (val === ''){
$('#player-name > .error-label').html('Please enter a name!')
} else return val as string
}
if (type === 'join'){
let val = $('#lobby-code > input').val()
if (val === ''){
$('#lobby-code > .error-label').html('Please enter your code!')
} else return val as string
}
return null
}

@ -0,0 +1,401 @@
class Player extends GameObject{
//Main class for the Players involved in the game
//including real life input and the paddleboard
//Settings
settings: Settings.Player
//Online players
lobby: Lobby
//General
color: Settings.Color
thisStroke: string
otherCount: number
//Position and dimensions
pos: Vector
side: Direction
dim: Vector
margin: number
//Movement
vel: Vector
absVel: number
acc: Vector
moveMargin: number
//Competitive
points: number
hasLost: boolean
//Boosts / Modifications
boosts: ActiveBoost[]
frameRate: number
vision: boolean
blinkProcess: TimeProcess
constructor(side: Direction, id: string, settings: Settings.Player, otherCount: number, lobby?: Lobby){
super(id)
this.settings = settings
this.lobby = lobby
this.otherCount = otherCount
this.side = side
this.setup(settings)
}
_input: Serialized.Player.Input
get input(): Serialized.Player.Input{
let input = {up: null, down: null, right: null, left: null};
if (this.isRight){
if (p.keyIsDown(38)) input.up = true;
if (p.keyIsDown(40)) input.down = true;
}
if (this.isLeft){
if (p.keyIsDown(87)) input.up = true;
if (p.keyIsDown(83)) input.down = true;
}
if (this.isTop){
if (p.keyIsDown(65)) input.left = true;
if (p.keyIsDown(68)) input.right = true;
}
if (this.isBottom){
if (p.keyIsDown(37)) input.left = true;
if (p.keyIsDown(39)) input.right = true;
}
return input;
}
set input(input: Serialized.Player.Input){
this._input = input
}
get startPosition(): Vector{
let s = this.settings
let side = this.side,
d = s.depth,
l = s.length,
m = s.margin,
cw = p.width,
ch = p.height
let x = (side === Direction.Left) ? m :
(side === Direction.Right) ? (cw - m - d) :
(side === Direction.Top || side === Direction.Bottom) ? (this.boxLeftSide + this.boxWidth / 2 - l / 2) : null
let y = (side === Direction.Left || side === Direction.Right) ? (this.boxTopSide + this.boxHeight / 2 - l / 2) :
(side === Direction.Top) ? m :
(side === Direction.Bottom) ? (ch - m - d) : null
if (x == null || y == null){
console.error('Something went wrong with computing the players positions. Side:', side)
}
return new Vector(x, y)
}
get startDimension(): Vector{
let s = this.settings
let d = s.depth,
l = s.length,
side = this.side
let x = (side === Direction.Left || side === Direction.Right) ? d :
(side === Direction.Top || side === Direction.Bottom) ? l : null
let y = (side === Direction.Left || side === Direction.Right) ? l :
(side === Direction.Top || side === Direction.Bottom) ? d : null
if (x == null || y == null){
console.error('Something went wrong with computing the players dimensions. Side:', side)
}
return new Vector(x, y)
}
get future(): Player{
let future = new Player(this.side, this.id, this.settings, this.otherCount)
future.pos = this.pos.copy()
future.vel = this.vel.copy()
future.move()
return future
}
get moveTopSide(): number{return this.boxTopSide + this.moveMargin}
get moveBottomSide(): number{return this.boxBottomSide - this.moveMargin}
get moveLeftSide(): number{return this.boxLeftSide + this.moveMargin}
get moveRightSide(): number{return this.boxRightSide - this.moveMargin}
get boxTopSide(): number{return (this.isTop ? 0 : (this.isBottom ? (p.height - this.margin) : (this.otherCount === 2 ? 0 : this.margin)))}
get boxBottomSide(): number {return (this.isTop ? this.margin : ((this.isBottom || this.otherCount != 4) ? p.height : p.height - this.margin))}
get boxLeftSide(): number {return this.isLeft ? 0 : (this.isRight ? p.width - this.margin : this.margin)}
get boxRightSide(): number {return this.isLeft ? this.margin : (this.isRight ? p.width : p.width - this.margin)}
get boxCenterX(): number {return (this.boxLeftSide + this.boxRightSide) / 2}
get boxCenterY(): number {return (this.boxTopSide + this.boxBottomSide) / 2}
get boxWidth(): number {return (this.isLeft || this.isRight) ? this.margin : p.width - 2 * this.margin}
get boxHeight(): number {return ((this.isTop || this.isBottom) ? this.margin : (this.otherCount === 2) ? p.height : (this.otherCount === 3) ? p.height - this.margin : p.height - 2 * this.margin)}
get isLeft(): boolean {return this.side === Direction.Left}
get isRight(): boolean {return this.side === Direction.Right}
get isTop(): boolean {return this.side === Direction.Top}
get isBottom(): boolean {return this.side === Direction.Bottom}
get leftSide(): number {return this.pos.x}
get rightSide(): number {return this.pos.x + this.dim.x}
get downSide(): number {return this.pos.y + this.dim.y}
get upSide(): number {return this.pos.y}
get toGameSide(): number {return this.isLeft ? this.rightSide : (this.isRight ? this.leftSide : (this.isTop ? this.downSide : (this.isBottom ? this.upSide : null)))}
get centerX(): number {return this.leftSide + this.width / 2}
get centerY(): number {return this.upSide + this.height / 2}
get width(): number {return this.dim.x}
get height(): number {return this.dim.y}
setup(settings: Settings.Player): void{
this._input = {up: false, down: false, left: false, right: false}
this.color = {
stroke: settings.color.stroke,
fill: settings.color.fill
}
this.thisStroke = settings.thisStroke
this.margin = settings.margin
this.pos = this.startPosition
this.dim = this.startDimension
this.vel = new Vector(0, 0)
this.absVel = settings.absVel
this.moveMargin = settings.moveMargin
this.points = settings.points
this.hasLost = false
this.boosts = []
this.vision = false
this.frameRate = 60
}
applyValues(state: Serialized.GameObject | {[key: string]: number}): void{
let recurse = (objIn: any, objOut: any): void => {
if (Array.isArray(objIn)){
for (let o1 of objOut){
let found = false
for (let o2 of objIn){
if (o2 === o1){
found = true
break
}
}
if (!found){
objOut.splice(objOut.indexOf(o1), 1)
}
}
}
for (let p in objIn){
if (typeof objIn[p] == 'object' && objIn[p] != null){
if (!objOut[p]) objOut[p] = {};
recurse(objIn[p], objOut[p])
} else objOut[p] = objIn[p]
}
}
recurse(state, this)
}
applyBoost(boost: Boost): void{
for (let b of this.boosts){
let index = this.boosts.indexOf(b)
if (b.type === boost.type){
this.boosts.splice(index, 1)
}
}
this.boosts.push(new ActiveBoost(this, boost))
}
reset(): void{
this.setup(this.settings);
}
checkInputs(online: boolean): void{
let input: Serialized.Player.Input
this.vel.mult(0)
if (online)
input = this._input
else
input = this.input
if (this.isLeft || this.isRight){
if (input.up && this.upSide > this.boxTopSide + this.moveMargin)
this.vel.y -= this.absVel
if (input.down && this.downSide < this.boxBottomSide - this.moveMargin)
this.vel.y += this.absVel
}
if (this.isTop || this.isBottom){
if (input.left && this.leftSide > this.boxLeftSide + this.moveMargin)
this.vel.x -= this.absVel
if (input.right && this.rightSide < this.boxRightSide - this.moveMargin)
this.vel.x += this.absVel
}
}
update(online: boolean): void{
this.checkInputs(online)
this.updateBoosts()
this.move()
this.updateBlink()
}
updateBoosts(): void{
for (let b of this.boosts){
b.update()
}
}
move(): void{
this.pos.add(this.vel)
}
equals(other: Player): boolean{
return this === other
}
lost(ball: Ball): void{
this.points--
this.blink({
stroke: 'rgb(255, 0, 0)',
fill: 'rgb(100, 0, 0)'
})
ball.reset(false)
if (!this.points) this.hasLost = true
}
blink(color: Settings.Color): void{
this.color = color
this.blinkProcess = new TimeProcess(this.settings.blinkTime)
}
updateBlink(): void{
if (this.blinkProcess){
this.blinkProcess.update()
if (this.blinkProcess.finished){
this.color = {
stroke: this.settings.color.stroke,
fill: this.settings.color.fill
}
this.blinkProcess = null
}
}
}
copy(): Player{
let copy = new Player(this.side, this.id, this.settings, this.otherCount)
copy.pos = this.pos.copy()
copy.vel = this.vel.copy()
return copy
}
serialized(): Serialized.Player{
let boosts: Serialized.ActiveBoost[] = []
for (let b of this.boosts) boosts.push(b.serialized())
return {
id: this.id,
pos: this.pos.serialized(),
vel: this.vel.serialized(),
dim: this.dim.serialized(),
margin: this.margin,
moveMargin: this.moveMargin,
color: this.color,
points: this.points,
lobby: this.lobby.serialized(),
input: this.input,
boosts: boosts,
frameRate: this.frameRate,
vision: this.vision
}
}
show(): void{
let stroke = this.color.stroke
if (game instanceof OnlineGame){
if (game.getThisPlayer().id === this.id) stroke = this.thisStroke
}
p.fill(this.color.fill)
p.stroke(stroke)
p.strokeWeight(2)
p.rect(this.leftSide, this.upSide, this.width, this.height, p.min([this.width, this.height]) * 0.5)
p.fill(0, 255, 0)
p.stroke(0)
p.strokeWeight(5)
if (this.isLeft || this.isRight){
p.line(this.leftSide, this.moveTopSide, this.rightSide, this.moveTopSide)
p.line(this.leftSide, this.moveBottomSide, this.rightSide, this.moveBottomSide)
}
if (this.isTop || this.isBottom){
p.line(this.moveLeftSide, this.upSide, this.moveLeftSide, this.downSide)
p.line(this.moveRightSide, this.upSide, this.moveRightSide, this.downSide)
}
p.textAlign('center', 'center')
p.textSize(20)
p.fill(0)
p.strokeWeight(3)
p.stroke(255, 0, 0)
p.text(p.str(this.points), this.boxCenterX, this.boxCenterY)
//TODO: Boost display for top and bottom players and add boost images
let positiveCount = 0,
negativeCount = 0
for (let b of this.boosts){
let positive = b.positive
if (positive) positiveCount++
else negativeCount++
let boxX,
boxY,
boxSize = this.margin * 0.8,
imgSize = boxSize * 0.7,
imgX = (boxSize - imgSize) / 2,
imgY = boxSize * 0.05,
barW = boxSize * 0.9,
barH = boxSize * 0.15,
barX = (boxSize - barW) / 2,
barY = boxSize - barH - boxSize * 0.05,
processW = barW * (1 - b.time.now / b.time.max),
margin = boxSize * 0.1
if (this.isLeft || this.isRight){
boxX = this.boxLeftSide + (this.margin - boxSize) / 2
boxY = this.boxCenterY - boxSize / 2 + (boxSize + margin) * (positive ? -positiveCount : negativeCount)
}
if (this.isTop || this.isBottom){
boxX = this.boxCenterX - boxSize / 2 + (boxSize + margin) * (positive ? -positiveCount : negativeCount)
boxY = this.boxTopSide + (this.margin - boxSize) / 2
}
//Box content
p.push()
p.translate(boxX, boxY)
//Bar
p.noFill()
p.stroke(0)
p.strokeWeight(1)
p.rect(barX, barY, barW, barH, barH / 5)
p.fill(b.color.fill)
p.rect(barX, barY, processW, barH, barH / 5)
//Image
p.image(icons[b.type][b.bool], imgX, imgY, imgSize, imgSize)
p.pop()
}
}
}

@ -0,0 +1,25 @@
class Vector extends p5.Vector {
x: number
y: number
constructor(x: number, y: number){
super(x, y);
}
addMag(length: number): Vector{
this.setMag(this.mag() + length)
return this
}
serialized(): Serialized.Vector{
return {
x: this.x,
y: this.y
};
}
copy(): Vector{
return new Vector(this.x, this.y)
}
}

@ -0,0 +1,59 @@
class Wormhole extends Item{
//Class for the wormhole objects appearing on the game board
//Inherits following properties from Item:
/*
id: string
pos: Vector
radius: number
color: Serialized.Color
settings: Settings.Item
time: TimeProcess
*/
power: number
constructor(settings: Settings.Wormhole){
super(settings)
this.power = settings.power
this.fadeTime = settings.fadeTime
this.radius = p.random(settings.minRadius, settings.maxRadius)
}
serialized(): Serialized.Wormhole{
let item = super.serialized() as Serialized.Wormhole
return item
}
destroy(): void{
let index = game.wormholes.indexOf(this)
game.wormholes.splice(index, 1)
}
attractBall(ball: Ball): void{
let distance = ball.dist(this.centerX, this.centerY)
let force = (this.power * ball.radius * this.currentRadius) / p.pow(distance, 2)
let forceVector = Vector.sub(this.pos, ball.pos).setMag(force) as Vector
ball.applyForce(forceVector)
}
show(): void{
let alpha = 0
p.push()
p.noFill()
p.strokeWeight(2)
for (let r = this.currentRadius; r > 0; --r){
let color = p.color(this.color.fill)
color.setAlpha(p.constrain(alpha, 0, 1) * 255)
p.stroke(color)
p.ellipse(this.centerX, this.centerY, r * 2, r * 2)
alpha += 1 / this.currentRadius;
}
p.pop()
}
}

@ -0,0 +1,2 @@
<?php
echo parse_ini_file("../../../server/.env")["HTTPS_PORT"];

@ -0,0 +1,8 @@
{
"collision": true,
"colorPicker": false,
"cookie": true,
"loader": false,
"prototypes": true,
"technical": true
}

@ -0,0 +1,131 @@
{
"project": {
"name": "pong",
"author": "BenjoCraeft",
"playerCounts": [2, 3, 4],
"online": {
"iceServers": [
{"urls": "stun:stun.l.google.com:19302"},
{
"urls": "turn:numb.viagenie.ca",
"credential": "muazkh",
"username": "webrtc@live.com"
}
]
}
},
"frameWork": {
"frameRate": 60,
"updateRate": 60,
"width": 1000,
"height": 1000
},
"game": {
"ball": {
"radius": 10,
"velocity": {
"start": 7,
"min": 5,
"runUpMax": 9,
"resetMultiplier": 0.8
},
"acceleration": {
"permanent": 0.007,
"max": 1
},
"runUp": {
"min": 1000,
"max": 3000
},
"color": {
"stroke": "rgb(0, 0, 220)",
"fill": "rgb(50, 50, 255)"
},
"safeRadius": 75
},
"player": {
"length": 200,
"depth": 12,
"margin": 80,
"points": 20,
"absVel": 6,
"color": {
"stroke": "rgb(0, 0, 0)",
"fill": "rgb(0, 0, 0)"
},
"thisStroke": "rgb(255, 255, 0)",
"moveMargin": 50,
"blinkTime": 200
},
"item": {
"boost": {
"radius": 30,
"types": [
"speed",
"border"
],
"color": {
"positive": {
"stroke": "rgb(0, 255, 0)",
"fill": "rgb(0, 100, 0)"
},
"negative": {
"stroke": "rgb(255, 0, 0)",
"fill": "rgb(100, 0, 0)"
}
},
"spawnTime": 4,
"duration": 20000,
"fadeTime": 500,
"effect": {
"speed": {
"duration": 15000,
"positive": {
"absVel": 9
},
"negative": {
"absVel": 3
},
"standard": {
"absVel": 6
}
},
"border": {
"duration": 10000,
"positive": {
"moveMargin": 0
},
"negative": {
"moveMargin": 100
},
"standard": {
"moveMargin": 50
}
}
}
},
"wormhole": {
"minRadius": 80,
"maxRadius": 120,
"fadeTime": 1000,
"power": 100,
"color": {
"fill": "rgb(0, 0, 0)",
"stroke": "rgb(0, 0, 0)"
},
"spawnTime": 20,
"duration": 5000
},
"newBall": {
"radius": 40,
"fadeTime": 1500,
"spawnTime": 15,
"duration": 15000,
"color": {
"fill": "rgb(0, 0, 50)",
"stroke": "rgb(0, 0, 255)"
}
}
}
}
}

@ -0,0 +1,88 @@
#color_picker{
width: 300px;
height: 25%;
margin: 20px;
margin-top: 50px;
border: 5px solid #000;
background-color: #000;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
}
#color_picker_numeric{
width: 80%;
padding: 5%;
margin: 5%;
background-color: #888;
border-radius: 10px;
overflow: hidden;
}
.color_picker_rgb{
float: left;
width: 22%;
height: 35px;
font-size: 25px;
color: #000;
}
.color_picker_rgb:nth-child(1){
margin-right: 10%;
margin-left: 3%;
background-color: #F00;
}
.color_picker_rgb:nth-child(2){
background-color: #0F0;
}
.color_picker_rgb:nth-child(3){
margin-left: 10%;
background-color: #00F;
color: #FFF;
}
#color_picker_hex{
width: 50%;
height: 30px;
font-size: 25px;
margin: 10% 25% 0 25%;
}
#saturation{
position: relative;
width: calc(100% - 33px);
height: 100%;
background: linear-gradient(to right, #FFF 0%, #F00 100%);
float: left;
margin-right: 6px;
}
#value {
width: 100%;
height: 100%;
background: linear-gradient(to top, #000 0%, rgba(255,255,255,0) 100%);
}
#sb_picker{
border: 2px solid;
border-color: #FFF;
position: absolute;
width: 14px;
height: 14px;
border-radius: 10px;
bottom: 50px;
left: 50px;
box-sizing: border-box;
z-index: 10;
}
#hue {
width: 27px;
height: 100%;
position: relative;
float: left;
background: linear-gradient(to bottom, #F00 0%, #F0F 17%, #00F 34%, #0FF 50%, #0F0 67%, #FF0 84%, #F00 100%);
}
#hue_picker {
position: absolute;
background: #000;
border-bottom: 1px solid #000;
top: 0;
width: 27px;
height: 2px;
}

Binary file not shown.

@ -0,0 +1,88 @@
input[type=range] {
-webkit-appearance: none;
margin: 18px 0;
width: 100%;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8.4px;
cursor: pointer;
animate: 0.2s;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #3071a9;
border-radius: 1.3px;
border: 0.2px solid #010101;
}
input[type=range]::-webkit-slider-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #ffffff;
cursor: pointer;
-webkit-appearance: none;
margin-top: -14px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #367ebd;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 8.4px;
cursor: pointer;
animate: 0.2s;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #3071a9;
border-radius: 1.3px;
border: 0.2px solid #010101;
}
input[type=range]::-moz-range-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #ffffff;
cursor: pointer;
}
input[type=range]::-ms-track {
width: 100%;
height: 8.4px;
cursor: pointer;
animate: 0.2s;
background: transparent;
border-color: transparent;
border-width: 16px 0;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #2a6495;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]::-ms-fill-upper {
background: #3071a9;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]::-ms-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #ffffff;
cursor: pointer;
}
input[type=range]:focus::-ms-fill-lower {
background: #3071a9;
}
input[type=range]:focus::-ms-fill-upper {
background: #367ebd;
}

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js" type="text/javascript"></script>
<script src="data/lib/socket.io-p2p.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js" type="text/javascript"></script>
<script src="https://code.jquery.com/jquery-3.6.4.min.js" type="text/javascript"></script>
<script src="data/scripts/js/main.js" type="text/javascript"></script>
<link href="styles.css" rel="stylesheet">
<link href="data/styles/color_picker.css" rel="stylesheet">
<link href="data/styles/range_input.css" rel="stylesheet">
<link href="data/images/favicon.ico" rel="icon" type="image/x-icon">
<title>Pong remastered online</title>
</head>
<body>
<div id="p5_loading"></div>
<div id="content">
<div class="border-node" id="interface">
<div id="game-controls">
<button class="paused" id="toggle-play" onclick="game.togglePlay(true, false);"></button>
</div>
<div id="setup-controls">
<fieldset class="border-node" id="game-type">
<legend>Type</legend>
<form>
<input id="offline" name="game-type" type="radio" value="offline">
<label for="offline">Offline</label><br>
<input id="online" name="game-type" type="radio" value="online">
<label for="online">Online</label>
</form>
</fieldset>
<div id="game-properties" style="display: none;">
<fieldset class="player-name border-node" id="player-name">
<legend>Your name</legend>
<input class="border-node" type="text"><br>
<span class="error-label"></span>
</fieldset>
<fieldset class="player-actions border-node" id="lobby-code">
<legend>Lobby code</legend>
<input class="border-node" type="text"><br>
<span class="error-label"></span>
</fieldset>
<div class="player-actions">
<button class="setup border-node" onclick="onlineManager.createLobby(this);">Create Lobby</button>
<span>Or</span>
<button class="setup border-node" onclick="onlineManager.joinLobby(this);">Join Lobby</button>
</div>
<button class="player-name start setup border-node" onclick="continueForm(this)">OK</button>
</div>
</div>
<div class="initialize-lobby" id="lobby">
<div class="border-node" id="lobby-members"></div>
<button class="setup border-node" id="start-lobby" onclick="onlineManager.startLobby(this);">Start</button>
<span>Code: </span>
<span></span>
<br>
<span class="error-label"></span>
</div>
</div>
<div class="border-node" id="canvasHolder"></div>
<div class="border-node" id="chat">
<div id="chat-content"></div>
<div id="chat-input">
<input type="text">
<button onclick="onlineManager.sendMessage()">Send</button>
</div>
</div>
<div id="loader"></div>
</div>
</body>
</html>

@ -0,0 +1,249 @@
{
"name": "pong-client",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pong-client",
"version": "1.0.0",
"dependencies": {
"socket.io-client": "^4.4.1"
},
"devDependencies": {
"@types/jquery": "^3.5.16",
"@types/node": "^18.15.10",
"typescript": "^5.0.2"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@types/jquery": {
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
"dev": true,
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/node": {
"version": "18.15.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz",
"integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==",
"dev": true
},
"node_modules/@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/socket.io-client": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz",
"integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.4.0",
"socket.io-parser": "~4.2.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/typescript": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
}
},
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"engines": {
"node": ">=0.4.0"
}
}
},
"dependencies": {
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@types/jquery": {
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/node": {
"version": "18.15.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz",
"integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==",
"dev": true
},
"@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"engine.io-client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"socket.io-client": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz",
"integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.4.0",
"socket.io-parser": "~4.2.1"
}
},
"socket.io-parser": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
}
},
"typescript": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
"dev": true
},
"ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
}
}
}

@ -0,0 +1,12 @@
{
"name": "pong-client",
"version": "1.0.0",
"devDependencies": {
"@types/jquery": "^3.5.16",
"@types/node": "^18.15.10",
"typescript": "^5.0.2"
},
"dependencies": {
"socket.io-client": "^4.4.1"
}
}

@ -0,0 +1,272 @@
a:link, a:hover, a:active, a:visited{color: #000;}
html, body{margin: 0; padding: 0; height: 100%; width: 100%;}
canvas{margin: 0; padding: 0; border: none; display: block;}
button:hover{cursor: pointer;}
@font-face{
font-family: "Rametto";
src: url("data/styles/font.ttf");
}
*{
font-family: "Rametto";
color: #000;
font-size: 17px;
}
:root{
--border-width: 2px;
--width: 1000px;
--height: 1000px;
--fieldset-width: 100px;
--body-background: rgb(60, 60, 100);
--interface-background: rgb(80, 80, 120);
--fieldset-background: rgb(100, 100, 130);
--input-background: rgb(120, 120, 150);
--lobby-leader-background: rgb(60, 60, 150);
--button-background: rgb(50, 180, 60);
--error-label-font: rgb(220, 50, 50);
--chat-first-background: rgb(50, 50, 170);
--chat-second-background: rgb(100, 100, 200);
--chat-writer-color: rgb(243, 130, 0);
}
body{
background-color: var(--body-background);
overflow: hidden;
}
#content{
position: absolute;
width: 100%;
height: 100%;
}
#canvasHolder{
width: var(--width);
height: var(--height);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#canvasHolder canvas{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: inherit;
}
#loader{
width: 70px;
height: 50px;
margin-top: 10px;
display: inline-block;
vertical-align: middle;
}
/**
* While playing
*/
#game-controls{
position: relative;
display: none;
width: 100%;
height: 100%;
text-align: center;
}
#toggle-play{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
background: transparent;
border: 0;
width: 0;
height: 100px;
border-color: transparent transparent transparent #000;
transition: 100ms all ease;
border-style: solid;
border-width: 50px 0px 50px 100px;
filter: drop-shadow(5px 5px 5px #000);
}
#toggle-play.paused{
border-style: double;
border-width: 0px 0px 0px 100px;
}
#toggle-play:hover{
border-color: transparent transparent transparent #111;
}
/**
* Interface with all it's controls
*/
#interface{
float: left;
background-color: var(--interface-background);
width: calc(50% - var(--width) / 2 - 2 * 10px - 4px);
height: calc(var(--height) - 10px);
}
/**
* Setup controls for creating games
*/
#setup-controls{
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
}
#setup-controls fieldset{
margin: 10px;
background-color: var(--fieldset-background);
max-width: var(--fieldset-width);
}
#setup-controls > fieldset{
margin: 30px;
}
#game-properties{
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
max-width: calc(var(--width) - 250px);
}
.error-label{
color: var(--error-label-font);
font-size: 13px;
font-weight: 100;
}
.border-node{
border: 2px solid #000;
border-radius: 10px;
}
input, select{
background-color: var(--input-background);
max-width: 200px;
}
select{
padding: 0px 20px 0px 0px;
font-size: 16px;
border: 1px solid #ccc;
height: 34px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: url(data/images/arrow.png) 100% / 50% no-repeat var(--input-background);
}
.setup{
margin: 10px;
background-color: var(--button-background);
}
.setup:hover, select:hover, input[type=text]:focus{
filter: brightness(80%);
}
.setup:disabled:hover{
filter: brightness(40%);
cursor: default;
}
.setup:disabled{
filter: brightness(40%);
}
/**
* Lobby
*/
#lobby-members{
background-color: var(--fieldset-background);
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
}
.lobby-member{
margin: 4px;
padding: 5px;
background-color: var(--fieldset-background);
cursor: pointer;
}
.lobby-member:hover{
filter: brightness(70%);
}
#lobby-leader{
background-color: var(--lobby-leader-background);
}
#lobby > span{
margin: 5px;
}
/**
* Chat
*/
#chat{
float: right;
background-color: var(--interface-background);
width: calc(50vw - var(--width) / 2 - 2 * 10px - 4px);
height: calc(var(--height) - 10px);
}
#chat-content{
border-radius: inherit;
height: calc(100% - 32px);
background-color: var(--fieldset-background);
width: 100%;
overflow-y: auto;
}
#chat-input{
width: 100%;
}
#chat-input > input[type="text"]{
max-width: 100%;
height: 100%;
width: calc(100% - 100px - 10px);
border-radius: 10px;
border: 1px solid #000;
}
#chat-input > button{
width: 100px;
background-color: var(--button-background);
border-radius: 10px;
border: 1px solid #000;
}
.message > span{
word-wrap: break-word;
font-size: 12px;
padding: 5px;
}
.message[name="0"]{
background-color: var(--chat-first-background)
}
.message[name="1"]{
background-color: var(--chat-second-background)
}
.message > span:first-child{
color: var(--chat-writer-color)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "es6",
"module": "amd",
"sourceMap": true,
"alwaysStrict": true,
"outFile": "./data/scripts/js/main.js"
}
}

@ -0,0 +1,2 @@
HTTPS_PORT=3103
SSL_PATH=/home/benjamin/code/web/ssl/benjamin-kraft.local

@ -0,0 +1,12 @@
[Mon Mar 27 2023 17:26:22][undefined] ---> {Server is listening on port 3103}
[Mon Mar 27 2023 17:31:06][pong] ---> {"beb(6Ya9ADw...)" connected}
[Mon Mar 27 2023 17:31:17][pong] ---> {"beb2(4XZozzj...)" connected}
[Mon Mar 27 2023 17:31:20][pong] ---> {"beb2(4XZozzj...)" created new lobby: "2DVLL2WEXI"}
[Mon Mar 27 2023 17:31:25][undefined] ---> {"beb(6Ya9ADw...)" tried to join non-existent lobby " 2DVLL2WEXI "}
[Mon Mar 27 2023 17:31:31][pong] ---> {"beb(6Ya9ADw...)" joined the lobby "2DVLL2WEXI"}
[Mon Mar 27 2023 17:31:36][pong] ---> {"beb2(4XZozzj...)" started the game: "2DVLL2WEXI"}
[Mon Mar 27 2023 17:33:13][pong] ---> {"beb(6Ya9ADw...)" left the lobby "2DVLL2WEXI"}
[Mon Mar 27 2023 17:33:13][pong] ---> {"beb(6Ya9ADw...)" disconnected}
[Mon Mar 27 2023 17:33:18][pong] ---> {"beb2(4XZozzj...)" left the lobby "2DVLL2WEXI"}
[Mon Mar 27 2023 17:33:18][pong] ---> {Lobby "2DVLL2WEXI" was deleted}
[Mon Mar 27 2023 17:33:18][pong] ---> {"beb2(4XZozzj...)" disconnected}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,18 @@
{
"name": "pong-server",
"version": "2.0",
"private": true,
"scripts": {
"start": "node out/index.js"
},
"dependencies": {
"dotenv": "^16.0.3",
"https": "^1.0.0",
"socket.io": "^4.4.1",
"socket.io-p2p-server": "^1.2.0",
"typescript": "^5.0.2"
},
"devDependencies": {
"@types/node": "^18.15.3"
}
}

@ -0,0 +1,168 @@
import {Room} from "./room.js"
import {ConnectionManager, serializeObject} from "./manager.js"
import {log} from "./logger.js";
import * as SocketIO from "socket.io";
export class Client {
socket: SocketIO.Socket;
name: string;
game: string;
id: string;
isReady: boolean;
isPlayer: boolean;
isSpectator: boolean;
constructor(socket: SocketIO.Socket, manager: ConnectionManager) {
this.socket = socket;
// @ts-ignore
this.name = socket.handshake.query.name;
// @ts-ignore
this.game = socket.handshake.query.game;
this.id = socket.id;
this.setEvents(manager)
}
get serialized(): Serialized.Client {
return {
id: this.id,
name: this.name,
game: this.game,
isReady: this.isReady,
isPlayer: this.isPlayer,
isSpectator: this.isSpectator
};
}
setEvents(mng: ConnectionManager): void {
let s = this.socket;
s.on('room-list', () => this.sendRoomList());
s.on('client-list', () => this.sendClientList());
s.on('set-ready', ready => this.setReady(ready));
s.on('game-settings', settings => this.setGameSettings(settings));
s.on('create-lobby', (settings, name) => this.createRoom(settings, name));
s.on('join-lobby', roomId => this.joinRoom(roomId));
s.on('leave-lobby', roomId => this.leaveRoom(roomId));
s.on('join-spectators', () => this.joinSpectators());
s.on('join-players', () => this.joinPlayers());
s.on('start-game', lobbyId => mng.startGame(this, lobbyId));
s.on('stop-game', lobbyId => mng.stopGame(this, lobbyId));
s.on('feedback', content => mng.saveFeedbackToFile(this, content));
s.on('disconnect', () => mng.disconnected(this));
this.send('connected')
}
sendRoomList(): void {
let rooms = ConnectionManager.RoomListByGame(this.game);
this.send('room-list', rooms)
}
sendClientList(): void {
let clients = ConnectionManager.ClientListByClientId(this.id);
this.send('client-list', clients)
}
setReady(ready: boolean): void {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (room) {
this.isReady = ready;
room.toAll('client-list', room.clients)
}
}
setGameSettings(settings: any): void {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (room) {
room.gameSettings = settings;
room.toAll('game-settings', settings)
}
}
createRoom(settings: Settings.Global, name: string): void {
let room = ConnectionManager.Instance.createRoom(settings, name);
room.add(this);
this.send('created-lobby', room);
log('lobby-created', this, room)
}
joinRoom(roomId: string): Room {
let room = Room.getByRoomId(roomId, ConnectionManager.Instance.rooms);
if (!room) {
this.send('join-failed', 'Room does not exist!');
log('join-non-existent', this, new Room('not-existent', roomId))
} else if (room.hasStarted && !room.settings.spectators) {
this.send('join-failed', 'Game has started yet!');
log('join-started', this, room)
} else {
room.add(this);
log('member-joined', this, room)
}
return room
}
leaveRoom(_roomId: string): void {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (!room)
return;
this.leave(room.id);
if (room.runningGame)
room.runningGame.removeClient(this);
room.clients.splice(room.clients.indexOf(this), 1);
room.toAll('member-left', this.id, this.name);
room.toAll('client-list', room.clients);
this.send('left-lobby');
log('member-left', this, room);
if (room.isEmpty && !room.settings.always) {
ConnectionManager.Instance.deleteRoom(room)
}
}
joinSpectators() {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (!room)
return;
this.isSpectator = true;
this.isPlayer = false;
room.toAll('client-list', room.clients)
}
joinPlayers() {
let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms);
if (!room)
return;
if (room.hasStarted)
return;
this.isSpectator = false;
this.isPlayer = true;
room.toAll('client-list', room.clients)
}
send(event: string, ...args: any[]): void {
this.socket.emit(event, ...serializeObject(args))
}
join(roomId: string): void {
this.socket.join(roomId)
}
leave(roomId: string): void {
this.socket.leave(roomId)
}
}

@ -0,0 +1,20 @@
declare namespace Serialized {
interface Lobby {
id: string
name: string
game: string
clientCounts: number[]
clients: Client[]
hasStarted: boolean
}
interface Client {
id: string
name: string
game: string
isReady: boolean
isPlayer: boolean
isSpectator: boolean
}
}

@ -0,0 +1,74 @@
declare module Settings {
interface Global {
project: Project
frameWork: FrameWork
game: any
always: boolean
spectators: boolean
}
interface Project {
name: string
author: string
playerCounts: number[]
}
interface FrameWork {
frameRate: number
updateRate: number
width: number
height: number
}
interface Game {
ball: Ball
player: Player
cw: number
ch: number
}
interface Ball {
radius: number
velocity: number
acceleration: number
runUp: Ball.RunUp
color: Color
cw: number
ch: number
}
interface Player {
width: number
height: number
margin: number
points: number
normal: State
weakened: State
enhanced: State
cw: number
ch: number
}
interface Color {
stroke: string
fill: string
}
interface State {
vel: Vector
color: Color
moveMargin: number
}
interface Vector {
x: number
y: number
}
module Ball {
interface RunUp {
min: number
max: number
}
}
}

@ -0,0 +1,37 @@
import {Room} from "./room.js"
import {Client} from "./client.js"
export class ServerGame {
room: Room;
settings: Settings.Global;
game: any;
constructor(room: Room, settings: Settings.Global) {
this.settings = settings;
this.room = room;
this.room.clients.forEach(c => this.addClient(c))
}
addClient(client: Client): void {
this.setEvents(client)
}
removeClient(client: Client): void {
this.removeEvents(client)
}
gameAction(action: string, ...args: any[]): void {
}
setEvents(client: Client): void {
let socket = client.socket;
socket.on('game-action', (action, ...args) => this.gameAction(action, ...args))
}
removeEvents(client: Client): void {
let socket = client.socket;
socket.removeAllListeners('game-action')
}
}

@ -0,0 +1,7 @@
import {Pong} from "./pong";
import {StartServer} from "./start";
StartServer({
useP2P: true,
gameClass: Pong
});

@ -0,0 +1,105 @@
import {Room} from "./room.js"
import {Client} from "./client.js"
import * as fs from "fs";
import * as util from "util";
let logFolder = "./logs";
if (!fs.existsSync(logFolder)) {
fs.mkdirSync(logFolder);
}
let logFile = fs.createWriteStream(logFolder + '/' + new Date().getTime() + '.log', {flags: 'a'});
let logStdout = process.stdout;
console.log = function () {
logFile.write(util.format.apply(null, arguments) + '\n');
logStdout.write(util.format.apply(null, arguments) + '\n');
};
console.error = console.log;
process.on('uncaughtException', err => {
console.error('Uncaught error: ', err);
process.exit(1);
});
process.stdin.pipe(logFile);
export function log(type: string, client: Client, lobby?: Room, msg?: string) {
let now = new Date(Date.now()).toString(), message, name, game;
let date = '[' + now.substring(0, now.indexOf('GMT') - 1) + ']';
if (client) {
game = '[' + client.game + ']';
let short = client.id.substring(0, Math.round(client.id.length / 3));
name = '"' + client.name + '(' + short + '...)"';
} else {
if (type === 'lobby-deleted') {
game = '[' + lobby.gameName + ']';
} else {
game = '[undefined]';
}
name = 'UNKNOWN';
}
if (lobby) {
game = '[' + lobby.gameName + ']';
}
switch (type) {
case 'join-non-existent':
message = name + ' tried to join non-existent lobby "' + lobby.id + '"';
break;
case 'join-started':
message = name + ' tried to join the started game "' + lobby.id + '"';
break;
case 'lobby-created':
message = name + ' created new lobby: "' + lobby.id + '"';
break;
case 'game-started':
message = name + ' started the game: "' + lobby.id + '"';
break;
case 'game-stopped':
message = name + ' stopped the game: "' + lobby.id + '"';
break;
case 'member-joined':
message = name + ' joined the lobby "' + lobby.id + '"';
break;
case 'member-left':
message = name + ' left the lobby "' + lobby.id + '"';
break;
case 'lobby-deleted':
message = 'Lobby "' + lobby.id + '" was deleted';
break;
case 'save-success':
message = msg;
break;
case 'save-error':
message = 'Failed to save contents to file: ' + msg;
break;
case 'load-success':
message = 'Successfully loaded and parsed file contents';
break;
case 'load-error':
message = 'Failed to load file: ' + msg;
break;
case 'parse-error':
message = 'Failed to parse contents: ' + msg;
break;
case 'feedback':
message = 'Saved feedback to file: ' + msg;
break;
case 'connection':
message = name + ' connected';
break;
case 'disconnection':
message = name + ' disconnected';
break;
case 'startup':
message = msg;
break;
}
console.log(date + game + ' ---> {' + message + '}');
}

@ -0,0 +1,148 @@
import {Room} from "./room.js"
import {Client} from "./client.js"
import {log} from "./logger.js"
import * as fs from "fs";
import * as SocketIO from "socket.io"
export class ConnectionManager {
static Instance: ConnectionManager;
io: SocketIO.Server;
rooms: Room[];
constructor(io: SocketIO.Server) {
ConnectionManager.Instance = this;
this.io = io;
this.rooms = [];
/*let drawSettings = {
project: {
name: 'global-draw',
playerCounts: null
},
always: true,
spectators: true
};
let drawRoom = this.createRoom(drawSettings, '');
drawRoom.id = 'global-draw-room';
drawRoom.startGame();
this.rooms.push(drawRoom);*/
}
static RoomListByGame(game: string): Room[] {
return this.Instance.rooms.filter(l => l.gameName === game)
}
static ClientListByClientId(clientId: string): Client[] {
let room = Room.getByClientId(clientId, this.Instance.rooms);
return room.clients
}
newSocket(socket: SocketIO.Socket): void {
let client = new Client(socket, this);
log('connection', client)
}
roomListUpdate(): void {
this.io.sockets.emit('room-list', serializeObject(this.rooms))
}
createRoom(settings: Settings.Global | any, name: string): Room {
let roomId = Room.generateCode(10);
let room = new Room(name, roomId, settings, this.io);
this.rooms.push(room);
this.roomListUpdate();
return room
}
deleteRoom(room: Room): void {
this.rooms.splice(this.rooms.indexOf(room), 1);
this.roomListUpdate();
log('lobby-deleted', null, room)
}
//Starts the game of a room with given id
startGame(client: Client, _roomId: string): void {
let lobby = Room.getByClientId(client.id, this.rooms);
if (!lobby) return;
if (!lobby.hasStarted) {
lobby.startGame();
log('game-started', client, lobby)
}
this.io.sockets.emit('room-list', serializeObject(this.rooms))
}
//Stops the game of a lobby with given id
stopGame(client: Client, lobbyId: string): void {
let lobby = Room.getByRoomId(lobbyId, this.rooms);
if (!lobby) return;
lobby.stopGame(client);
log('game-stopped', client, lobby)
}
//Saves user feedback to a file
saveFeedbackToFile(client: Client, content: string): void {
let date = new Date(Date.now()).toString();
let path = "feedback/" + client.game + '.txt';
let saveToFile = (content: string) => {
fs.writeFile(path, content, (err: any) => {
if (err)
log('save-error', client, null, err.message);
else
log('feedback', client, null, path)
});
};
if (fs.existsSync(path)) {
fs.readFile(path, 'utf8', (err, data) => {
if (err)
log('load-error', client, null, err.message);
else {
log('load-success', client, null);
let newContent = data + '\n\n\n\n' + date + '\n\n' + content;
saveToFile(newContent)
}
})
} else {
saveToFile(date + '\n' + content)
}
}
//Removes a disconnected client from all references
disconnected(client: Client): void {
let room = Room.getByClientId(client.id, this.rooms);
if (room)
client.leaveRoom(room.id);
log('disconnection', client)
}
}
export function serializeObject(object: any): any {
function serialize(obj: any) {
if (!obj)
return obj;
if (obj.serialized)
return obj.serialized;
else if (obj instanceof Array) {
let content = [];
obj.forEach(o => {
content.push(serialize(o))
});
return content
}
return obj
}
return serialize(object)
}

@ -0,0 +1,10 @@
import {ServerGame} from "./game_standard"
import {Room} from "./room";
export class Pong extends ServerGame {
constructor(lobby: Room, settings: Settings.Global) {
super(lobby, settings)
}
}

@ -0,0 +1,135 @@
import {Client} from "./client.js"
import {ServerGame} from "./game_standard.js"
import {serializeObject} from "./manager.js";
import {Server} from "socket.io";
export class Room {
id: string;
gameName: string;
clientCounts: number[];
io: Server;
clients: Client[];
runningGame: ServerGame;
settings: Settings.Global;
gameSettings: any;
name: string;
static GameClass: typeof ServerGame
constructor(name: string, id: string, settings?: Settings.Global, io?: Server) {
this.id = id;
this.name = name;
if (!io || !settings) return;
this.settings = settings;
this.gameName = settings.project.name;
this.clientCounts = settings.project.playerCounts;
this.io = io;
this.clients = [];
this.gameSettings = {}
}
get leader(): Client {
return this.players[0]
}
get players(): Client[] {
return this.clients.filter(c => c.isPlayer)
}
get spectators(): Client[] {
return this.clients.filter(c => c.isSpectator)
}
get serialized(): Serialized.Lobby {
return {
id: this.id,
name: this.name,
game: this.gameName,
clientCounts: this.clientCounts,
clients: serializeObject(this.clients),
hasStarted: this.hasStarted
};
}
get isEmpty(): boolean {
return !(this.clients.length)
}
get hasStarted(): boolean {
return this.runningGame != null
}
static getByRoomId(id: string, lobbies: Room[]): Room {
for (let l of lobbies) {
if (l.id === id)
return l
}
return null;
}
static getByClientId(id: string, lobbies: Room[]): Room {
for (let l of lobbies) {
for (let c of l.clients) {
if (c.id === id)
return l
}
}
return null;
}
static generateCode(elements: number): string {
let code = '';
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
while (elements--) {
code += possible.charAt(Math.floor(Math.random() * possible.length))
}
return code
}
startGame(): void {
let seed = Math.random() * 10000;
this.toAll('start-game', seed);
this.runGame()
}
stopGame(client: Client): void {
this.toAll('stop-game', client);
this.runningGame = null
}
add(client: Client): void {
this.clients.push(client);
let isPlayer = !this.hasStarted && this.hasValidPlayerCount();
client.isPlayer = isPlayer;
client.isSpectator = !isPlayer;
client.isReady = false;
client.join(this.id);
this.toAll('member-joined', client.id, client.name);
this.toAll('client-list', this.clients);
this.toAll('game-settings', this.gameSettings);
if (this.hasStarted)
this.runningGame.addClient(client)
}
hasValidPlayerCount(): boolean {
let valid = false;
this.clientCounts.forEach(c => {
if (c === this.clients.length)
valid = true
});
return valid
}
runGame(): void {
this.runningGame = new Room.GameClass(this, this.settings);
}
toAll(event: string, ...args: any[]): void {
this.io.to(this.id).emit(event, serializeObject(this), ...serializeObject(args))
}
}

@ -0,0 +1,39 @@
import {ConnectionManager} from "./manager.js";
import {log} from "./logger.js";
import {Server} from 'socket.io';
import {Room} from "./room.js";
import * as https from "https";
import * as fs from "fs";
export function StartServer(settings: any){
require("dotenv").config();
const httpsPort = parseInt(process.env.HTTPS_PORT);
let cert = fs.readFileSync(`${process.env.SSL_PATH}/cert.pem`);
let key = fs.readFileSync(`${process.env.SSL_PATH}/key.pem`);
let httpsServer = https.createServer({key: key, cert: cert});
let sIO = new Server(httpsServer, {
cors: {
origin: ["https://play.benjamin-kraft.local", "https://play.benjamin-kraft.eu"]
}
});
if (settings.useP2P){
const p2p = require('socket.io-p2p-server').Server;
sIO.use(p2p);
}
httpsServer.listen(httpsPort);
Room.GameClass = settings.gameClass;
let connectionManager = new ConnectionManager(sIO);
// On new connection
sIO.on('connection', socket => connectionManager.newSocket(socket));
log('startup', null, null, 'Server is listening on port ' + httpsPort);
}

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "CommonJS",
"outDir": "./out",
"sourceMap": true,
"alwaysStrict": true
},
"include": [
"./src"
]
}
Loading…
Cancel
Save