You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
536 lines
12 KiB
536 lines
12 KiB
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
|
|
}
|
|
}
|
|
|
|
} |