diff --git a/game/Fireworks.js b/game/Fireworks.js new file mode 100644 index 0000000..573bf7b --- /dev/null +++ b/game/Fireworks.js @@ -0,0 +1,226 @@ +import Util from './../common/Util.js' + +let minVx = -10 +let deltaVx = 20 +let minVy = 2 +let deltaVy = 15 +const minParticleV = 5 +const deltaParticleV = 5 + +const gravity = 1 + +const explosionRadius = 200 +const bombRadius = 10 +const explodingDuration = 100 +const explosionDividerFactor = 10 + +const nBombs = 1 +const percentChanceNewBomb = 5 + +function color() { + const r = Util.randomInt(0, 255) + const g = Util.randomInt(0, 255) + const b = Util.randomInt(0, 255) + return 'rgba(' + r + ',' + g + ',' + b + ', 0.8)' +} + +// A Bomb. Or firework. +class Bomb { + constructor() { + this.radius = bombRadius + this.previousRadius = bombRadius + this.explodingDuration = explodingDuration + this.hasExploded = false + this.alive = true + this.color = color() + + this.px = (window.innerWidth / 4) + (Math.random() * window.innerWidth / 2) + this.py = window.innerHeight + + this.vx = minVx + Math.random() * deltaVx + this.vy = (minVy + Math.random() * deltaVy) * -1 // because y grows downwards + + this.duration = 0 + } + + update(particlesVector) { + if (this.hasExploded) { + const deltaRadius = explosionRadius - this.radius + this.previousRadius = this.radius + this.radius += deltaRadius / explosionDividerFactor + this.explodingDuration-- + if (this.explodingDuration == 0) { + this.alive = false + } + } + else { + this.vx += 0 + this.vy += gravity + if (this.vy >= 0) { // invertion point + this.explode(particlesVector) + } + + this.px += this.vx + this.py += this.vy + } + } + + draw(ctx) { + ctx.beginPath() + ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false) + if (!this.hasExploded) { + ctx.fillStyle = this.color + ctx.lineWidth = 1 + ctx.fill() + } + } + + explode(particlesVector) { + this.hasExploded = true + const e = 3 + Math.floor(Math.random() * 3) + for (let j = 0; j < e; j++) { + const n = 10 + Math.floor(Math.random() * 21) // 10 - 30 + const speed = minParticleV + Math.random() * deltaParticleV + const deltaAngle = 2 * Math.PI / n + const initialAngle = Math.random() * deltaAngle + for (let i = 0; i < n; i++) { + particlesVector.push(new Particle(this, i * deltaAngle + initialAngle, speed)) + } + } + } +} + +class Particle { + constructor(parent, angle, speed) { + this.px = parent.px + this.py = parent.py + this.vx = Math.cos(angle) * speed + this.vy = Math.sin(angle) * speed + this.color = parent.color + this.duration = 40 + Math.floor(Math.random() * 20) + this.alive = true + } + update() { + this.vx += 0 + this.vy += gravity / 10 + + this.px += this.vx + this.py += this.vy + this.radius = 3 + + this.duration-- + if (this.duration <= 0) { + this.alive = false + } + } + + draw(ctx) { + ctx.beginPath() + ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false) + ctx.fillStyle = this.color + ctx.lineWidth = 1 + ctx.fill() + } +} + +class Controller { + constructor(canvas) { + this.canvas = canvas + this.ctx = this.canvas.getContext('2d') + this.resize() + + window.addEventListener('resize', () => { + this.resize() + }) + } + + setSpeedParams() { + let heightReached = 0 + let vy = 0 + + while (heightReached < this.canvas.height && vy >= 0) { + vy += gravity + heightReached += vy + } + + minVy = vy / 2 + deltaVy = vy - minVy + + const vx = (1 / 4) * this.canvas.width / (vy / 2) + minVx = -vx + deltaVx = 2 * vx + } + + resize() { + this.setSpeedParams() + } + + init() { + this.readyBombs = [] + this.explodedBombs = [] + this.particles = [] + + for (let i = 0; i < nBombs; i++) { + this.readyBombs.push(new Bomb()) + } + } + + update() { + if (Math.random() * 100 < percentChanceNewBomb) { + this.readyBombs.push(new Bomb()) + } + + const aliveBombs = [] + while (this.explodedBombs.length > 0) { + const bomb = this.explodedBombs.shift() + bomb.update() + if (bomb.alive) { + aliveBombs.push(bomb) + } + } + this.explodedBombs = aliveBombs + + const notExplodedBombs = [] + while (this.readyBombs.length > 0) { + const bomb = this.readyBombs.shift() + bomb.update(this.particles) + if (bomb.hasExploded) { + this.explodedBombs.push(bomb) + } + else { + notExplodedBombs.push(bomb) + } + } + this.readyBombs = notExplodedBombs + + const aliveParticles = [] + while (this.particles.length > 0) { + const particle = this.particles.shift() + particle.update() + if (particle.alive) { + aliveParticles.push(particle) + } + } + this.particles = aliveParticles + } + + render() { + this.ctx.beginPath() + this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' // Ghostly effect + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) + + for (let i = 0; i < this.readyBombs.length; i++) { + this.readyBombs[i].draw(this.ctx) + } + + for (let i = 0; i < this.explodedBombs.length; i++) { + this.explodedBombs[i].draw(this.ctx) + } + + for (let i = 0; i < this.particles.length; i++) { + this.particles[i].draw(this.ctx) + } + } +} + +export default Controller diff --git a/game/game.js b/game/game.js index 2d28021..6edd9f2 100644 --- a/game/game.js +++ b/game/game.js @@ -7,6 +7,7 @@ import Communication from './Communication.js' import Util from './../common/Util.js' import PuzzleGraphics from './PuzzleGraphics.js' import Game from './Game.js' +import fireworksController from './Fireworks.js' if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]' if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]' @@ -319,6 +320,14 @@ async function main() { // Create a dom and attach adapters to it so we can work with it const canvas = addCanvasToDom(Graphics.createCanvas()) + + const longFinished = Game.getFinishTs(gameId) + let finished = longFinished ? true : false + const justFinished = () => !!(finished && !longFinished) + + const fireworks = new fireworksController(canvas) + fireworks.init(canvas) + const ctx = canvas.getContext('2d') // initialize some view data @@ -391,6 +400,7 @@ async function main() { } break; } } + finished = Game.getFinishTs(gameId) }) let _last_mouse_down = null @@ -438,6 +448,12 @@ async function main() { } Communication.sendClientEvent(evt) } + + finished = Game.getFinishTs(gameId) + if (justFinished()) { + fireworks.update() + RERENDER = true + } } const onRender = async () => { @@ -522,6 +538,9 @@ async function main() { if (DEBUG) Debug.checkpoint('scores done') // --------------------------------------------------------------- + if (justFinished()) { + fireworks.render() + } RERENDER = false } diff --git a/server/Game.js b/server/Game.js index 6dc128d..a25d582 100644 --- a/server/Game.js +++ b/server/Game.js @@ -23,7 +23,12 @@ function loadAllGames() { } const file = `${DATA_DIR}/${f}` const contents = fs.readFileSync(file, 'utf-8') - const game = JSON.parse(contents) + let game + try { + game = JSON.parse(contents) + } catch { + console.log(`[ERR] unable to load game from file ${f}`); + } if (typeof game.puzzle.data.started === 'undefined') { game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs) }