-
-
Save sharafian/956a11a8f6f767f78c7cd1a321529e7e to your computer and use it in GitHub Desktop.
Monetized Game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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