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 } } }