Skip to content

Instantly share code, notes, and snippets.

@sharafian
Created August 19, 2019 22:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sharafian/956a11a8f6f767f78c7cd1a321529e7e to your computer and use it in GitHub Desktop.
Save sharafian/956a11a8f6f767f78c7cd1a321529e7e to your computer and use it in GitHub Desktop.
Monetized Game
import React, { Component } from 'react'
const skyColors = [
'LightBlue',
'DeepSkyBlue',
'CornflowerBlue',
'RoyalBlue',
'DarkBlue',
'MidnightBlue',
'Black'
]
const WIDTH = 640
const HEIGHT = 480
const totalDays = (skyColors.length - 2) * 7
const prices = { goblin: 100000, pentagram: 1000000 }
const inputEvents = []
const globalGameState = {
x: 0,
mx: -500,
days: 0,
cash: 0,
crops: [],
pentagrams: [],
candles: [],
goblinCount: 0,
finalCash: 0,
parade: [],
paradeCount: 0,
linkHover: false,
tool: 'goblin',
errorMessage: '',
errorMessageExpire: 0
}
export class MonetizedGame extends Component {
componentDidMount () {
this.canvas = document.getElementById('monetized_game')
this.ctx = this.canvas.getContext('2d')
this.lastFrameDate = Date.now()
requestAnimationFrame(this._renderFrame.bind(this))
document.monetization &&
document.monetization.addEventListener('monetizationprogress', ev => {
globalGameState.cash += Math.max(
Number(ev.detail.amount),
10000
)
})
this.gsprites = new Image()
this.gsprites.src = '/gob.png'
this.psprites = new Image()
this.psprites.src = '/pentagram.png'
this.skyGradient = this.ctx.createLinearGradient(0, 0, 0, 150)
this.skyGradient.addColorStop(0, skyColors[1])
this.skyGradient.addColorStop(1, skyColors[0])
document.addEventListener('keyup', ev => {
if (ev.key === 'p') {
globalGameState.tool = 'pentagram'
} else if (ev.key === 'g') {
globalGameState.tool = 'goblin'
}
})
const offsetLeft = this.canvas.offsetLeft
const offsetTop = this.canvas.offsetTop
this.canvas.addEventListener('mousemove', ev => {
const x = ev.pageX - offsetLeft
const y = ev.pageY - offsetTop
globalGameState.linkHover = x > 30 && x < 570 && y > 180 && y < 210
})
this.canvas.addEventListener('mouseup', ev => {
const x = ev.pageX - offsetLeft
const y = ev.pageY - offsetTop
if (globalGameState.days > totalDays) {
if (globalGameState.linkHover) {
window.open('https://twitter.com/intent/tweet?text='
+ encodeURIComponent(`I made ${globalGameState.goblinCount} goblins in ` +
`goblin farmer! @sharafian_ https://sharafian.com/goblin`),
'_blank')
}
} else {
inputEvents.push({
action: 'click',
x, y: y - 10
})
}
})
}
_insertGoblin (x, y, age = 0) {
let depth = 0
for (depth = 0; depth < globalGameState.crops.length; ++depth) {
if (y < globalGameState.crops[depth].y) {
break
}
}
globalGameState.crops.splice(depth, 0, {
x, y,
age,
walking: false,
walkingSpeed: 50 + Math.random() * 100,
frame: 0
})
}
_insertCandle (x, y, opts) {
let depth = 0
for (depth = 0; depth < globalGameState.candles.length; ++depth) {
if (y < globalGameState.candles[depth].y) {
break
}
}
globalGameState.candles.splice(depth, 0, Object.assign({
x, y,
frame: 0
}, opts))
}
_insertPentagram (x, y) {
globalGameState.pentagrams.push({
x, y,
age: 0,
frame: 0
})
// top candle
this._insertCandle(x, y - 9, {
vframe: 1,
offsetY: 11
})
// middle candle
this._insertCandle(x, y + 2, {
vframe: 2,
offsetY: 22
})
// bottom candle
this._insertCandle(x, y + 10, {
vframe: 3,
offsetY: 30
})
}
_timeStep () {
const frameDate = Date.now()
const timeStepMs = frameDate - this.lastFrameDate
this.lastFrameDate = frameDate
return timeStepMs / 1000
}
_logicStep (timeStep) {
if (globalGameState.days > totalDays) {
if (!globalGameState.parade[0] || globalGameState.parade[0].x > 20) {
if (globalGameState.paradeCount++ < globalGameState.goblinCount) {
globalGameState.parade.unshift({
frame: 0,
x: -10,
y: 160
})
}
}
for (let i = 0; i < globalGameState.parade.length; i++) {
const goblin = globalGameState.parade[i]
if (goblin.x > WIDTH + 10) {
globalGameState.parade.splice(i, 1)
i--
continue
}
goblin.frame += timeStep
goblin.x += timeStep * 50
}
return
}
// loop it around
const newGlobalStateX = (globalGameState.x + timeStep * 200) %
(WIDTH + 100)
const newGlobalStateMX = (globalGameState.mx + timeStep * 200) %
(WIDTH + 100)
if (newGlobalStateX < globalGameState.x) {
globalGameState.days ++
if (globalGameState.days > totalDays) {
globalGameState.finalCash = globalGameState.cash
return
}
const skyColor = Math.floor(globalGameState.days / 7)
this.skyGradient = this.ctx.createLinearGradient(0, 0, 0, 150)
this.skyGradient.addColorStop(0, skyColors[skyColor + 1])
this.skyGradient.addColorStop(1, skyColors[skyColor])
for (let i = 0; i < globalGameState.crops.length; ++i) {
const crop = globalGameState.crops[i]
if (++crop.age >= 7 && !crop.walking) {
globalGameState.cash += prices.goblin * 2
globalGameState.goblinCount ++
crop.walking = true
}
if (crop.walking && crop.x > WIDTH + 10) {
globalGameState.crops.splice(i, 1)
i--
}
}
for (const pentagram of globalGameState.pentagrams) {
if (pentagram.age++ % 3 === 0) {
this._insertGoblin(pentagram.x, pentagram.y, 5)
}
}
}
for (const pentagram of globalGameState.pentagrams) {
pentagram.frame += timeStep
}
for (const candle of globalGameState.candles) {
candle.frame += timeStep
}
for (const crop of globalGameState.crops) {
crop.frame += timeStep
if (crop.walking) {
crop.x += crop.walkingSpeed * timeStep
}
}
globalGameState.x = newGlobalStateX
globalGameState.mx = newGlobalStateMX
let ev
while (ev = inputEvents.pop()) {
if (ev.action === 'click') {
let isPent = globalGameState.tool === 'pentagram'
let rad = isPent ? 20 : 10
if (ev.x < 10 || ev.x > WIDTH - 10 ||
ev.y < 160 || ev.y > HEIGHT - 10) {
this._setErrorMessage('units can only be placed in the field')
continue
}
let notColliding = true
for (const crop of globalGameState.crops) {
notColliding = notColliding && (
(ev.x + rad < crop.x - 10 || ev.x - rad > crop.x + 10) ||
(ev.y + rad < crop.y - 10 || ev.y - rad > crop.y + 10)
)
}
for (const pent of globalGameState.pentagrams) {
notColliding = notColliding && (
(ev.x + rad < pent.x - 20 || ev.x - rad > pent.x + 20) ||
(ev.y + rad < pent.y - 20 || ev.y - rad > pent.y + 20)
)
}
if (!notColliding) {
this._setErrorMessage('that\'s too close to another unit')
continue
}
if (globalGameState.cash < prices[globalGameState.tool]) {
this._setErrorMessage('insufficient funds for ' + globalGameState.tool)
continue
}
globalGameState.cash -= prices[globalGameState.tool]
// dismiss on successful action
this._setErrorMessage('', 0)
if (globalGameState.tool === 'goblin') {
this._insertGoblin(ev.x, ev.y)
} else {
this._insertPentagram(ev.x, ev.y)
}
}
}
}
_setErrorMessage (msg, duration = 2000) {
globalGameState.errorMessage = msg
globalGameState.errorMessageExpire = Date.now() + duration
}
_pad (amount) {
return ' '.repeat(Math.max(4 - String(Math.floor(amount / 10000)).length, 0))
}
_renderFrame () {
const ctx = this.ctx
const timeStep = this._timeStep()
this._logicStep(timeStep)
if (globalGameState.days > totalDays) {
ctx.fillStyle = 'MidnightBlue'
ctx.fillRect(0, 0, WIDTH, HEIGHT)
ctx.fillStyle = 'white'
ctx.font = '20px monospace'
ctx.fillText('Game Over.', 40, 40)
ctx.fillText(
`You made ${Math.floor(globalGameState.finalCash / 10000)} gold`,
40, 80)
ctx.fillText(
`You created ${globalGameState.goblinCount} goblins for your army.`,
40, 120)
const img = this.gsprites
for (const crop of globalGameState.parade) {
const frame = 7 + (Math.floor(crop.frame * 10) % 3)
ctx.drawImage(
img,
frame * 20,
0,
20,
20,
crop.x - 10,
crop.y - 10,
20,
20)
}
ctx.fillStyle = globalGameState.linkHover
? 'red'
: 'white'
ctx.strokeStyle = globalGameState.linkHover
? 'red'
: 'white'
ctx.font = '20px monospace'
ctx.strokeRect(30, 180, 540, 30)
ctx.fillText(
`Click this link to tweet your score!`,
40, 200)
requestAnimationFrame(this._renderFrame.bind(this))
return
}
ctx.fillStyle = this.skyGradient
ctx.fillRect(0, 0, WIDTH, HEIGHT)
ctx.fillStyle = 'darkseagreen'
ctx.fillRect(0, 150, WIDTH, HEIGHT - 150)
ctx.fillStyle = 'black'
ctx.fillRect(0, 0, WIDTH, 30)
ctx.fillStyle = 'white'
ctx.font = '16px monospace'
ctx.fillText(
`day ${globalGameState.days}/${totalDays}, ` +
this._pad(globalGameState.cash) +
`${Math.floor(globalGameState.cash / 10000)} gold, ` +
`click to plant ${globalGameState.tool} | tools: g p`,
10, 20)
const pimg = this.psprites
for (const pent of globalGameState.pentagrams) {
const frame = Math.floor(pent.frame * 5) % 3
ctx.drawImage(
pimg,
frame * 40,
0,
40,
40,
pent.x - 20,
pent.y - 20,
40,
40)
}
const img = this.gsprites
let gi = 0
let ci = 0
while (gi < globalGameState.crops.length || ci < globalGameState.candles.length) {
const crop = globalGameState.crops[gi]
const candle = globalGameState.candles[ci]
if (!candle || (crop && crop.y < candle.y)) {
const frame = crop.walking
? 7 + (Math.floor(crop.frame * 10 * (crop.walkingSpeed / 100)) % 3)
: Math.min(Math.floor(crop.frame * 5), crop.age)
gi++
ctx.drawImage(
img,
frame * 20,
0,
20,
20,
crop.x - 10,
crop.y - 10,
20,
20)
} else {
const frame = Math.floor(candle.frame * 5) % 3
ci++
ctx.drawImage(
pimg,
frame * 40,
candle.vframe * 40,
40,
40,
candle.x - 20,
candle.y - candle.offsetY,
40,
40)
}
}
ctx.fillStyle = 'yellow'
ctx.fillRect(globalGameState.x - 50, 50, 50, 50)
ctx.fillStyle = 'white'
ctx.fillRect(globalGameState.mx - 50, 50, 50, 50)
const now = Date.now()
if (now < globalGameState.errorMessageExpire) {
ctx.fillStyle = 'red'
ctx.font = '16px monospace'
ctx.fillText(globalGameState.errorMessage, 10, HEIGHT - 10)
}
requestAnimationFrame(this._renderFrame.bind(this))
}
render () {
return <>
<canvas id='monetized_game' width={WIDTH} height={HEIGHT} />
</>
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment