This Site's JavaScript
Much like my literate CSS system, I’ve implemented a literate JS system for this site. Naturally, the features implemented here are only available on a web browser, and not when browsing via Org mode. The JS here is purely for easter eggs on my site; the page will function perfectly well with JS disabled.
Vim Bindings
I’ve implemented simple vim-like bindings for scrolling the page with j
and k
:
document.addEventListener('keydown', function(e) {
switch (e.key) {
case 'j':
window.scrollBy(0, 20);
break;
case 'k':
window.scrollBy(0, -20);
break;
case 'J':
window.scrollBy(0, window.innerHeight / 2);
break;
case 'K':
window.scrollBy(0, -window.innerHeight / 2);
break;
}; })
This code is mostly self explanatory; pressing j
or k
scrolls the page by 20px vertically, and J
and K
scroll by half a page at a time.
Cookies
On the front page of my site, there’s a cookie banner. If you read closely, you’ll notice it isn’t an ordinary cookie banner. If you then click Manage cookies
, it takes you to a window where you can manage your cookies.
Cookie management here consists of the accumulation of cookies and cookie producing objects in an homage to Orteil’s cookie clicker. This is a bare-bones reimplementation of the basics of the game.
State
All of the game’s state and configuration (read: magic numbers) is kept in a single data structure. On each tick of the game, new values for cookie counts are computed based on the cost and CPS of each autoclicker, and these computed values are stored back in the current state.
Here’s the default values used when loading a new game:
const defaultState = {
cookies: 0, // Number of cookies the player has
cps: 0, // Total cookies per second
lastPlayed: 0, // Unix timestamp of the last time the game was played
acs: { // Set of all autoclickers available for purchase
cursors: {count: 0, cost: 15, cps: 0.1},
grandmas: {count: 0, cost: 100, cps: 1},
farms: {count: 0, cost: 1.1e3, cps: 8},
mines: {count: 0, cost: 12e3, cps: 47},
factories: {count: 0, cost: 130e3, cps: 260},
banks: {count: 0, cost: 1.4e6, cps: 1.43e3},
temples: {count: 0, cost: 20e6, cps: 7.8e3},
wizards: {count: 0, cost: 330e6, cps: 44e3},
spaceships: {count: 0, cost: 5.1e9, cps: 260e3},
alchemists: {count: 0, cost: 75e9, cps: 1.6e6},
portals: {count: 0, cost: 1e12, cps: 10e6},
timeMachines: {count: 0, cost: 14e12, cps: 65e6}
} }
By default, we’ll initialise our game’s state to this:
let state = defaultState
Outside of the internal game state, there is also some necessary state for the game loop to run. First, we want to keep track of whether or not the game has been started:
let started = false // Has the game started?
Next, ticker
stores the reference for the javascript object that calls the tick
function later on. We need it globally so that we can stop the ticker arbitrarily, and we declare the variable here to keep things in one place.
let ticker = null // ref to interval for ticker
We’ll set the TPS of the game to 20. Values past this aren’t really noticeable in my opinion:
const tps = 20 // Ticks per second
Display Code
The numbers in cookie clicker can quickly get quite big, so we want a way to display large numbers in a readable format. The following function pretty prints a number in engineering notation:
function engineering(number) {
const suffixes = ['', 'K', 'M', 'B', 'T'];
const fix = Number.isInteger(number) ? 0 : 2
if (number < 1000) return number.toFixed(fix)
const exponent = Math.floor(Math.log10(number) / 3);
const rounded = (number / Math.pow(1000, exponent)).toFixed(fix);
const suffix = (exponent >= suffixes.length)
? `e${exponent*3}`
: `${suffixes[exponent]}`
return `${rounded}${suffix}`
}
Now we need a helper to use the correct singular/plural form of a given unit:
function displayUnit(value, singular, plural) {
// Display a value with a unit in either singular or plural form
let unit = (value == 1) ? singular : plural
return `${engineering(value)} ${unit}`
}
Here’s another helper that displays the name, amount, and cost of an autoclicker:
function displayAC(name) {
// Update the count and cost for an autoclicker
document.getElementById(name).innerHTML = state.acs[name].count
document.getElementById("buy"+name).innerHTML = displayUnit(state.acs[name].cost, "cookie", "cookies")
}
And a quick function to display the current cookies per second:
function displayCPS() {
// Display the current cookies per second
document.getElementById("cps").innerHTML = `${engineering(state.cps)} CPS`
}
Save and Restore
An idle game that doesn’t save its state isn’t very fun to play. We can save the state locally via LocalStorage:
function save() {
// Save the game to local storage
.lastPlayed = Date.now()
state.setItem("state", JSON.stringify(state))
localStorage }
This stores the entire state structure defined above into local storage. It can thus be modified by the user to cheat. I did say it was bare-bones.
We obviously need to load our state back in for it to be of any use. Loading the saved state is straightforward; but we want to compute the cookies produced in the time the game was closed. Since we saved the time of the last save point, we can compute the number of seconds elapsed since the save occurred, and multiply that by the cookies per second to get the updated value. If I was evil, I’d add an offline penalty here too. We have a quick check to prevent someone from cheating by changing their local clock, mainly because that’s a boring way to cheat.
function restore() {
// Restore from local storage if the game was saved
let savedState = JSON.parse(localStorage.getItem("state"))
if (!savedState) return
= savedState
state console.log("Restored the following state:", state)
// Compute cookies generated during idle time
= Date.now()
now if (state.lastPlayed < now) {
= now - state.lastPlayed
delta .cookies += state.cps * delta / 1000
state.lastPlayed = now
state
} }
Finally, we’ll add a reset functionality (with a confirmation!) to erase the save state.
function reset() {
// Reset the game
if (confirm("Resetting will erase all progress! Are you sure?")) {
.removeItem("state")
localStorage= defaultState
state .reload(true) // Reload the page to refresh displayed values
location
} }
Game Logic
First off, we want a helper function that calculates the current CPS based on the currently owned autoclickers.
function updateCPS() {
= 0
total for (let i in state.acs) {
+= state.acs[i].count * state.acs[i].cps
total
}.cps = total
state }
Here’s the main tick function. Since the game is so simple, this effectively is a one-liner:
function tick() {
// Perform one tick of the game
.cookies += state.cps / tps
statedocument.getElementById("cookies").innerHTML = displayUnit(state.cookies, "cookie", "cookies")
}
When starting the game up, we need to set all of our default values. init
loads the saved state, starts the ticker, and displays the shop values for the autoclickers:
function init() {
// Initialise the game values and start the ticker
= true
started restore()
= window.setInterval(tick, 1000/tps)
ticker for (let name in state.acs) displayAC(name)
displayCPS()
console.log("Started cookie clicker")
}
Once the game is closed, we want to stop the ticker to save on battery life/CPU cycles:
function stop() {
// Stop the game
= false
started clearInterval(ticker)
}
User Interaction
Clicking the cookie is a very important feature! I use the cookie button for two purposes; to start the game if it’s not already running (a check which may no longer be necessary), and to increment the number of cookies. This action automatically saves the game.
// USER INTERACTION
function click_cookie() {
// Start the game if it hasn't been started already, and add a cookie
if (!started) init()
.cookies++
statesave()
}
Saving for and purchasing autoclickers forms the main gameplay loop. If the player has enough cookies to buy an autoclicker, then we remove the cost from their balance, increment the amount of that autoclicker, increase its cost by 15%, update our displays, and save the game.
function buy(name) {
// Buy an autoclicker
let item = state.acs[name]
if (state.cookies >= item.cost) {
.cookies -= item.cost
state.count++
item.cost *= 1.15
itemupdateCPS()
save()
displayAC(name)
displayCPS()
} }
The Cookie Banner
Finally, we need to be able to hide/show the cookie banner and the “manage cookies” page itself. This could be done in CSS, but I decided to throw it in JS to keep things in one place. These functions are all called via onclick
tags in the page’s HTML.
function hideBar() {
= document.getElementById("cookie_bar")
bar .classList.toggle("fadeOut");
bar
}
function showSettings() {
= document.getElementById("cookie_settings")
settings .style.visibility = "visible"
settings
}
function hideSettings() {
= document.getElementById("cookie_settings")
settings .classList.toggle("fadeOut")
settings }