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

2 years ago
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
}
}
}