Comedy game with flying
// Declaration of constants | |
levels = [ | |
{ | |
title: "1: You make your coworkers laugh", | |
score: 0, | |
max: 30, | |
emoji: { | |
bomb: 9, | |
drink: 30, | |
nobombing: 1000 | |
} | |
}, | |
{ | |
title: "2: You can do an open mic in Ohio", | |
score: 20, | |
max: 40, | |
emoji: { | |
bomb: 9, | |
drink: 30, | |
nobombing: 2000 | |
} | |
}, | |
{ | |
title: "3: An open mic with real audience", | |
score: 80, | |
max: 50, | |
emoji: { | |
bomb: 8, | |
drink: 30, | |
nobombing: 100 | |
} | |
}, | |
{ | |
title: "4: Killed it at an amature show in a bar", | |
score: 160, | |
emoji: { | |
bomb: 7, | |
drink: 30, | |
nobombing: 700 | |
} | |
}, | |
{ | |
title: "5: A set in The Setup San Francisco", | |
score: 260, | |
emoji: { | |
bomb: 6, | |
drink: 25, | |
nobombing: 1000 | |
} | |
}, | |
{ | |
title: "6: Destroyed it at the Punchline SF", | |
score: 380, | |
emoji: { | |
bomb: 5, | |
drink: 40, | |
nobombing: 1000 | |
} | |
}, | |
{ | |
title: "7: Murdered again at the Punchline SF", | |
score: 420, | |
emoji: { | |
bomb: 4, | |
drink: 30, | |
nobombing: 100 | |
} | |
}, | |
{ | |
title: "8: Sleep at the Comedy Store LA", | |
score: 600, | |
emoji: { | |
bomb: 3, | |
drink: 30, | |
nobombing: 2000 | |
} | |
}, | |
{ | |
title: "9: Comedy Cellar NYC might call you", | |
score: 800, | |
emoji: { | |
bomb: 2, | |
drink: 35, | |
nobombing: 100 | |
} | |
}, | |
{ | |
name: "10: Scheduled a Netflix Special", | |
score: 1200, | |
emoji: { | |
bomb: 1, | |
drink: 35, | |
nobombing: 700 | |
} | |
}, | |
{ | |
name: "11: You're the comedy god'", | |
score: 1500, | |
emoji: { | |
bomb: 0, | |
drink: 30, | |
nobombing: 400 | |
} | |
} | |
] | |
const laughFiles = [ | |
'laugh1.m4a', | |
'laugh2.m4a', | |
'laugh3.mp3', | |
'laugh4.mp3', | |
] | |
const booSound = new Audio('/static/sounds/boo.m4a'); | |
const drinkSound = new Audio('/static/sounds/drink.mp3'); | |
drinkSound.volume = 0.5; | |
const laugh = () => { | |
const laughFile = laughFiles[rand(laughFiles.length)]; | |
const audio = new Audio('/static/sounds/' + laughFile); | |
audio.volume = 0.1 * (rand(4) + 1); | |
audio.play(); | |
} | |
const emoji = { | |
bomb: "💣", | |
drink: "🍺", | |
nobombing: "🍹", | |
laugh: "😂", | |
rolling: "🤣" | |
} | |
// Class of the single emoji | |
class Emoji { | |
constructor(x, y, house, type = null) { | |
house.total++; | |
this.x = x; | |
this.y_ = y; | |
this.createdTime = new Date(); | |
this.lastTime = new Date(); | |
this.size_ = 20 + rand(80); | |
this.speed = rand(30) | |
this.rotate_ = 0; | |
this.house = house; | |
this.score = 1; | |
if (!type) { | |
if (this.condition(0, 10)) type = 'rolling'; | |
if (this.condition(100, levels[this.house.levelIndex].emoji.nobombing, !this.house.noBombing)) type = 'nobombing'; | |
if (this.condition(50, levels[this.house.levelIndex].emoji.drink, !this.house.noBombing)) type = 'drink'; | |
if (this.condition(20, levels[this.house.levelIndex].emoji.bomb, !this.house.noBombing)) type = 'bomb'; | |
} | |
if (!type) type = 'laugh'; | |
this.type_ = type; | |
this.emoji = emoji[type]; | |
} | |
condition(min, chance, condition) { | |
if (this.house.total > min) { | |
if (rand(chance) === 0 && condition) { | |
return true; | |
} | |
} | |
return false; | |
} | |
get type() { | |
return { | |
[this.type_]: true | |
} | |
} | |
get fontSize() { | |
if (this.exploded_time_) { | |
const mil = new Date - this.exploded_time_; | |
if (this.disappear_) { | |
const unit = (mil / 100) < 1 ? 1 : mil / 100 | |
const fontSize = Math.round((this.size_ / (unit))); | |
return fontSize + 'px serif'; | |
} else { | |
return this.size_ + Math.round(mil / this.speed * 10) + 'px serif'; | |
} | |
} | |
return this.size_ + 'px serif'; | |
} | |
get y() { | |
const mil = new Date - this.lastTime; | |
if (!this.house.gameOver) this.y_ = this.exploded_time_ ? this.exploded_y_ : (this.y_ - Math.round(mil / this.speed)); | |
this.lastTime = new Date; | |
return this.y_; | |
//return this.y_ - Math.round(mil / this.speed); | |
} | |
get alpha() { | |
if (this.type.bomb && !this.disappear_) { | |
return 1 | |
} | |
if (this.exploded_time_) { | |
const mil = new Date - this.exploded_time_; | |
const a = 1 - mil / 1000; | |
return a < 0 ? 0 : a; | |
} | |
return 1; | |
} | |
render(ctx) { | |
const { x, y } = this; | |
ctx.save(); | |
ctx.globalAlpha = this.alpha; | |
ctx.textBaseline = 'middle'; | |
ctx.textAlign = "center"; | |
ctx.font = this.fontSize; | |
ctx.fillText((this.house.gameOver && !this.type.bomb) ? "👿" : this.emoji, this.x, this.y); | |
ctx.restore(); | |
if (y < -this.size_) { | |
this.destroy(); | |
this.house.missed++; | |
} | |
} | |
disappear() { | |
this.exploded_y_ = this.y; | |
this.exploded_time_ = new Date(); | |
this.disappear_ = true; | |
setTimeout(() => this.destroy(), 2000); | |
} | |
explode() { | |
if (this.type.bomb) { | |
this.house.gameOver = true; | |
booSound.play(); | |
} | |
this.exploded_y_ = this.y; | |
this.exploded_time_ = new Date(); | |
if (this.type.bomb) { | |
setTimeout(() => { | |
this.house.updateScore(); | |
this.destroy() | |
this.house.gameOver = false; | |
}, 3000); | |
this.house.score -= 10; | |
this.house.loseLife(); | |
return; | |
} | |
if (this.type.drink) { | |
this.house.setMode('drink'); | |
drinkSound.play(); | |
setTimeout(() => this.destroy(), 1000); | |
return; | |
} | |
if (this.type.nobombing) { | |
this.house.setMode('nobombing'); | |
setTimeout(() => this.destroy(), 1000); | |
return; | |
} | |
this.emoji = "💥"; | |
laugh(); | |
setTimeout(() => this.destroy(), 1000); | |
this.house.score += this.score; | |
} | |
checkCollision(mx, my) { | |
if (this.exploded_time_) return; | |
const { x, y } = this; | |
const half = this.size_ / 2 + 20; | |
if ((mx > x - half) && (mx < x + half) && (my > y - half) && (my < y + half)) { | |
this.explode(); | |
} | |
} | |
destroy() { | |
const index = this.house.emojiList.indexOf(this); | |
this.house.emojiList.splice(index, 1); | |
} | |
} | |
// Class of the game | |
class Game { | |
constructor(elPath, scorePath, embedded = false) { | |
window.game = this; | |
this.embedded = embedded; | |
this.canvas = document.querySelector(elPath); | |
this.scoreboard = document.querySelector(scorePath); | |
} | |
reset() { | |
this.gameOver = false; | |
this.endGame = false; | |
} | |
end() { | |
this.gameContainer.classList.remove('game-show'); | |
} | |
start() { | |
this.gameContainer = document.querySelector(".game-container"); | |
this.gameContainer.classList.add('game-show'); | |
const game = document.querySelector("#game"); | |
game.width = window.innerWidth; | |
game.height = window.innerHeight; | |
this.gameOver = false; | |
this.total = 0; | |
this.score_ = 0; | |
this.missed_ = 0; | |
this.scoreList = []; | |
this.interval = 2000; | |
this.levelIndex = 0; | |
this.totalLifes = 4; | |
this.lifes_ = this.totalLifes; | |
this.gameOverElement = document.querySelector("#gameover"); | |
this.currentLevelEl = document.querySelector(".your-level"); | |
this.mx = 0; | |
this.my = 0; | |
const section = this.embedded ? this.canvas.parentElement : this.canvas; | |
this.ctx = this.canvas.getContext('2d'); | |
this.canvas.setAttribute('width', this.canvas.clientWidth); | |
this.canvas.setAttribute('height', this.canvas.clientHeight); | |
this.emojiList = []; | |
this.objectList = []; | |
this.isTouchDevice = isTouchDevice(); | |
if (this.isTouchDevice) { | |
setTimeout(() => { | |
const rect = section.getBoundingClientRect() | |
section.addEventListener('touchstart', (e) => { | |
this.touchStarted = true; | |
this.mx = e.touches[0].clientX; | |
this.my = e.touches[0].clientY; | |
this.emojiList.forEach(e => e.checkCollision(this.mx, this.my)); | |
}) | |
section.addEventListener('touchmove', (e) => { | |
this.mx = e.touches[0].clientX; | |
this.my = e.touches[0].clientY; | |
this.emojiList.forEach(e => e.checkCollision(this.mx, this.my)); | |
}) | |
section.addEventListener('touchend', (e) => { | |
this.touchStarted = false; | |
this.mx = -1000; | |
this.my = -1000; | |
}) | |
section.addEventListener('touchcancel', (e) => { | |
this.touchStarted = false; | |
this.mx = -1000; | |
this.my = -1000; | |
}) | |
}, 1000); | |
} else { | |
section.addEventListener('mousemove', (e) => { | |
this.mx = e.clientX; | |
this.my = e.clientY; | |
}) | |
} | |
this.level = levels[this.levelIndex].title; | |
this.createdTime = new Date(); | |
requestAnimationFrame(this.draw.bind(this)); | |
this.speed = 1000; | |
this.tick(); | |
this.updateScore(); | |
} | |
get speed() { | |
return this.speed_; | |
} | |
set speed(value) { | |
this.speed_ = value; | |
} | |
setMode(mode) { | |
switch (mode) { | |
case 'nobombing': | |
if(this.noBombing) clearTimeout(this.noBombing); | |
this.noBombing = setTimeout(() => { | |
this.noBombing = null; | |
}, 10000); | |
this.emojiList.forEach(e => { | |
if (e.type.bomb) e.disappear(); | |
}); | |
this.sendMessage("🚫💣"); | |
break; | |
case 'drink': | |
this.emojiList.forEach(e => { | |
if (e.type.bomb) e.disappear(); | |
}); | |
break; | |
default: | |
} | |
} | |
sendMessage(message) { | |
} | |
get livesLeft() { | |
if (this.lifes_ < 0 || this.totalLifes - this.lifes_ < 0) return; | |
const life = "❤️" | |
const bomb = "🖤" | |
return life.repeat(this.lifes_) + bomb.repeat(this.totalLifes - this.lifes_); | |
} | |
announceGameOver() { | |
this.gameOverElement.classList.add('gameover-show') | |
} | |
loseLife() { | |
this.lifes_--; | |
if (this.lifes_ === 0) { | |
// Game over; | |
this.gameOver = true; | |
this.endGame = true; | |
this.announceGameOver(); | |
} | |
} | |
tick() { | |
if (this.timeout) clearTimeout(this.timeout); | |
this.getScoreLastSeconds(5); | |
const interval = this.noBombing ? 50 : this.scoreLastSeconds > 3 ? this.speed / (this.scoreLastSeconds / 3) : this.speed; | |
if (this.speed !== 0) { | |
this.timeout = setTimeout(() => { | |
if (!this.gameOver) this.addEmoji(); | |
this.tick(); | |
}, interval / 2 + rand(interval)) | |
} | |
} | |
addEmoji() { | |
if (this.emojiList.length > (levels[this.levelIndex].max || 50)) return; | |
if (this.gameOver) return; | |
if (this.endGame) return; | |
const e = new Emoji(Math.floor(Math.random() * this.canvas.width), this.canvas.height, this) | |
this.emojiList.push(e) | |
} | |
get score() { | |
return this.score_; | |
} | |
set score(v) { | |
if (levels[this.levelIndex + 1].score <= v) { | |
this.currentLevelEl.classList.remove("blink"); | |
setTimeout(() => this.currentLevelEl.classList.add("blink"), 50); | |
this.levelIndex++; | |
this.level = levels[this.levelIndex].title; | |
} | |
this.scoreList.push({ | |
date: Date.now(), | |
value: v - this.score_ | |
}) | |
this.score_ = v; | |
this.updateScore(); | |
} | |
get missed() { | |
return this.missed_; | |
} | |
getScoreLastSeconds(seconds) { | |
const cut = Date.now() - seconds * 1000; | |
let nobread = true; | |
for (let i = 0; i < this.scoreList.length; i++) { | |
if (this.scoreList[i].date > cut) { | |
nobread = false; | |
if (i > 0) this.scoreList.splice(0, i - 1); | |
break; | |
} | |
} | |
if (nobread) this.scoreList = []; | |
this.scoreLastSeconds = this.scoreList.length | |
return this.scoreLastSeconds; | |
} | |
set missed(v) { | |
this.missed_ = v; | |
this.updateScore(); | |
} | |
updateScore() { | |
if (!this.scoreboard) return; | |
if (!this.scoreboardElements) { | |
this.scoreboardElements = {}; | |
['score', 'missed', 'livesLeft', 'level'].forEach(param => { | |
this.scoreboardElements[param] = document.querySelector(`#score-${param}`) | |
}); | |
} | |
['score', 'missed', 'livesLeft', 'level'].forEach(param => { | |
this.scoreboardElements[param].innerText = this[param]; | |
}); | |
//this.scoreboardElements.level.className = 'level-' + this.levelIndex; | |
} | |
// Main game loop | |
draw() { | |
const time = Date.now(); | |
if (!this.lastTime) this.lastTime = time; | |
if ((this.lastTime + this.interval) < time) { | |
this.lastTime = time; | |
} | |
if (!this.isTouchDevice) this.emojiList.forEach(e => e.checkCollision(this.mx, this.my)); | |
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
this.emojiList.forEach(e => e.render(this.ctx)); | |
this.objectList.forEach(e => e.render(this.ctx)); | |
if (!this.isTouchDevice || this.touchStarted) { | |
this.ctx.textBaseline = 'middle'; | |
this.ctx.textAlign = "center"; | |
this.ctx.font = '70px serif';; | |
this.ctx.fillText(" 🎤", this.mx, this.my); | |
} | |
requestAnimationFrame(this.draw.bind(this)); | |
} | |
} | |
// Game init | |
const theGame = new Game('#game', "#score"); | |
theGame.start(); | |
// Helper functions | |
function rand(i) { | |
return Math.floor(Math.random() * i); | |
} | |
function isTouchDevice() { | |
try { | |
document.createEvent("TouchEvent"); | |
return true; | |
} catch (e) { | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment