"use strict"; const belongings = {}; const stats = {}; const macroDesc = { name: "Fen", species: "crux", proSubject: "he", proObject: "him", proPossessive: "his", } const ownedUpgrades = {}; const remainingUpgrades = []; let showOwnedUpgrades = false; const effects = {}; const resources = {}; let updateRate = 60; const currentProductivity = {}; const contributions = {}; const clickPowers = { clickBonus: 0, clickMultiplier: 1, clickVictim: "micro", clickSeconds: 10 } let clickBonus = 0; let clickVictim = "micro"; let lastTime = 0; let controlHeld = false; let shiftHeld = false; let mouseTarget = undefined; let newsShowTimer; let newsRemoveTimer; const newsDelay = 8000; const newsWeightFactors = []; let buttonClicked = false; const state = { ownedUpgrades: ownedUpgrades, resources: resources, currentProductivity: currentProductivity, belongings: belongings, clickPowers: clickPowers, stats: stats }; const numberModes = { words: { name: "Words", render: numberText, next: "smallWords" }, smallWords: { name: "Small Words", render: numberTextSmall, next: "scientific" }, scientific: { name: "Scientific", render: numberScientific, next: "full", }, full: { name: "Full", render: numberFull, next: "words" } } deepFreeze(numberModes); let numberMode = numberModes["words"]; const activePowerups = {}; function tickPowerups(delta) { const powerupList = document.querySelector("#powerup-list"); let changed = false; // I love mutating arrays as I traverse them. Object.entries(activePowerups).filter(x => x[1].life > 0).forEach(([key, data]) => { const newLife = data.life - delta; if (newLife <= 0) { setTimeout(() => { powerupList.removeChild(data.element); }, 1000); data.element.classList.add("powerup-entry-done"); activePowerups[key].life = 0; changed = true; } else { data.life = newLife; const frac = (data.maxLife - data.life) / (data.maxLife); data.element.style.setProperty("--progress", frac * 100 + "%") } }); if (changed) { updateAll(); } } function addPowerup(key, powerup) { // powerup already exists if (activePowerups[key].life > 0) { activePowerups[key].life += powerup.duration; activePowerups[key].maxLife = activePowerups[key].life; } else { const powerupList = document.querySelector("#powerup-list"); const powerupEntry = document.createElement("div"); powerupEntry.classList.add("powerup-entry"); const powerupIconHolder = document.createElement("div"); powerupIconHolder.classList.add("powerup-entry-icon-holder"); const powerupIcon = document.createElement("i"); powerupIcon.classList.add("fas"); powerupIcon.classList.add(powerup.icon); powerupIconHolder.appendChild(powerupIcon); powerupEntry.appendChild(powerupIconHolder); powerupList.appendChild(powerupEntry); activePowerups[key] = {powerup: powerup, life: powerup.duration, maxLife: powerup.duration, element: powerupEntry }; powerupEntry.addEventListener("mousemove", function (e) { powerupTooltip(key, e); }); powerupEntry.addEventListener("mouseleave", function () { powerupTooltipRemove(); }); updateAll(); } } function applyGlobalProdBonus(cost) { for (let effect of effects["prod-all"]) { if (ownedUpgrades[effect.parent]) { effect.apply(cost); } } } function calculateProductivity() { let productivity = makeCost(); for (const [key, value] of Object.entries(belongings)) { const provided = productivityOf(key); productivity = addCost(productivity, provided); contributions[key] = provided; } return productivity; } // here's where upgrades will go :3 function applyProductivityMultipliers(type, cost) { for (let effect of effects["prod"]) { if (ownedUpgrades[effect.parent] && effect.target == type) { effect.apply(cost); } } for (let effect of effects["helper"]) { if (ownedUpgrades[effect.parent] && effect.helped == type) { effect.apply(cost, belongings[effect.helper].count); } } } function productivityOf(type) { let baseProd = makeCost(buildings[type].prod); applyProductivityMultipliers(type, baseProd); applyGlobalProdBonus(baseProd); scaleCost(baseProd, belongings[type].count); return baseProd; } function makeCost(source) { const empty = mapObject(resourceTypes, () => 0); return {...empty, ...source}; } function addCost(cost1, cost2) { return Object.keys(resourceTypes).reduce((o, k) => {o[k] += cost2[k]; return o;}, cost1); } function scaleCost(cost, scale) { return Object.keys(resourceTypes).reduce((o, k) => {o[k] *= scale; return o;}, cost); } function costOfBuilding(type, count = 1) { let total = makeCost(); while (count > 0) { let baseCost = makeCost(buildings[type].cost); baseCost = scaleCost(baseCost, Math.pow(1.15, belongings[type].count + count - 1)); total = addCost(total, baseCost); count--; } return mapObject(total, round); } function buildingCount() { if (controlHeld) { return 10; } else if (shiftHeld) { return 100; } else { return 1; } } function buyBuilding(type, e) { const count = buildingCount(); let cost = costOfBuilding(type, count); if (canAfford(cost)) { spend(cost); belongings[type].count += count; } updateProductivity(); } function updateAll() { updateProductivity(); updateClickVictim(); updateOptions(); } function updateOptions() { cache.optionButtons.numbers.innerText = "Number mode: " + numberMode.name; } // update stuff function updateDisplay() { let newTime = performance.now(); let delta = newTime - lastTime; lastTime = newTime; addResources(delta); displayResources(); displayBuildings(); displayUpgrades(showOwnedUpgrades); tickPowerups(delta); Object.keys(statTypes).forEach(key => { const value = document.querySelector("#stat-value-" + key); value.innerText = render(stats[key]); }) stats.seconds += delta / 1000; setTimeout(updateDisplay, 1000 / updateRate); } function updateProductivity() { Object.assign(currentProductivity, calculateProductivity()); // maybe this should go somewhere else - it also does clicking... updateClickPowers(); Object.entries(activePowerups).forEach(([key, entry]) => { if (entry.life > 0) { const powerup = entry.powerup; powerup.effect(state); } }); } function addResources(delta) { for (const [resource, amount] of Object.entries(currentProductivity)) { const gained = amount * delta / 1000; resources[resource] += gained; if (resource == "food") stats.food += gained; } } function displayResources() { document.title = "Gorge - " + round(resources.food) + " food"; Object.keys(resources).forEach(key => { cache.resourceLabels[key].quantity.innerText = render(resources[key]) + " " + resourceTypes[key].name; if (resourceTypes[key].generated) cache.resourceLabels[key].rate.innerText = render(currentProductivity[key]) + " " + resourceTypes[key].name + "/sec"; }) } function displayBuildings() { const count = buildingCount(); for (const [key, value] of Object.entries(belongings)) { let available = states.buildings[key].available; if (!belongings[key].visible) { if (resources.food * 10 >= costOfBuilding(key).food) { unlockBuilding(key); } if (belongings[key].count > 0) { unlockBuilding(key); } else { continue; } belongings[key].visible = true; let button = cache.buildingButtons[key].button; button.classList.remove("hidden"); } let button = cache.buildingButtons[key].button; let name = cache.buildingButtons[key].name; let cost = cache.buildingButtons[key].cost; const buildingCost = costOfBuilding(key, count); const newName = value.count + " " + (value.count == 1 ? buildings[key].name : buildings[key].plural); if (newName != states.buildings[key].name) { name.innerText = newName; states.buildings[key].name = newName; } const newCost = render(buildingCost.food) + " food"; if (newCost != states.buildings[key].cost) { cost.innerText = newCost; states.buildings[key].cost = newCost; } if (canAfford(buildingCost) && available !== true) { button.classList.remove("building-button-disabled"); cost.classList.add("building-button-cost-valid"); states.buildings[key].available = true; } else if (!canAfford(buildingCost) && available !== false) { button.classList.add("building-button-disabled"); cost.classList.add("building-button-cost-invalid"); states.buildings[key].available = false; } } } function canAfford(cost) { for (const [resource, amount] of Object.entries(cost)) { if (resources[resource] < amount) { return false; } } return true; } function spend(cost) { for (const [resource, amount] of Object.entries(cost)) { resources[resource] -= amount; } } function switchShowOwnedUpgrades() { initializeUpgradeStates(); if (showOwnedUpgrades) { document.querySelector("#upgrades").innerText = "Upgrades"; } else { document.querySelector("#upgrades").innerText = "Owned Upgrades"; } showOwnedUpgrades = !showOwnedUpgrades; } function displayUpgrades(owned) { if (owned) { Object.entries(ownedUpgrades).forEach(([key, val]) => { let button = cache.upgradeButtons[key]; if (val) { button.classList.remove("hidden"); } else { button.classList.add("hidden"); } }); } else { for (let id of remainingUpgrades) { let button = cache.upgradeButtons[id]; let visible = states.upgrades[id].visible; let available = states.upgrades[id].available; if (ownedUpgrades[id] && visible !== false) { button.classList.add("hidden"); states.upgrades[id].visible = false; continue; } if (upgradeReachable(id) && visible !== true) { button.classList.remove("hidden"); states.upgrades[id].visible = true; } else if (!upgradeReachable(id) && visible !== false) { button.classList.add("hidden"); states.upgrades[id].visible = false; } if (upgradeAvailable(id) && available !== true) { button.classList.remove("upgrade-button-inactive"); states.upgrades[id].available = true; } else if (!upgradeAvailable(id) && available !== false) { button.classList.add("upgrade-button-inactive"); states.upgrades[id].available = false; } } // we aren't trimming the list of upgrades now // because we need to switch between owned and unowned upgrades // - thus we need to be able to show or hide anything /* for (let i = remainingUpgrades.length-1; i >= 0; i--) { if (ownedUpgrades[remainingUpgrades[i]]) { remainingUpgrades.splice(i, 1); } }*/ } } function updateClickPowers() { let bonus = 0; clickPowers.clickMultiplier = 1; for (let effect of effects["click"]) { if (ownedUpgrades[effect.parent]) { bonus = effect.apply(bonus, currentProductivity["food"]); } } clickPowers.clickBonus = bonus; } function updateClickVictim() { const button = document.querySelector("#tasty-micro"); button.classList.remove(...button.classList); for (let i=effects["click-victim"].length - 1; i >=0; i--) { const effect = effects["click-victim"][i]; if (ownedUpgrades[effect.parent]) { clickPowers.clickVictim = effect.id; button.classList.add("fas") button.classList.add(buildings[effect.id].icon) return; } } clickPowers.clickVictim = "micro"; button.classList.add("fas") button.classList.add(buildings.micro.icon) } function buyUpgrade(id, e) { if (ownedUpgrades[id]) { return; } let upgrade = upgrades[id]; if (!upgradeAvailable(id)) { return; } spend(upgrade.cost); ownedUpgrades[id] = true; let text = "Bought " + upgrade.name + "!"; clickPopup(text, "upgrade", [e.clientX, e.clientY]); updateProductivity(); updateClickVictim(); } function eatPrey() { const add = clickPowers.clickMultiplier * (buildings[clickPowers.clickVictim]["prod"].food * clickPowers.clickSeconds + clickPowers.clickBonus); resources.food += add; stats.foodClicked += add; return add; } // setup stuff lol // we'll initialize the dict of buildings we can own function setup() { // create static data createTemplateUpgrades(); // prepare dynamic stuff initializeData(); initializeNews(); createButtons(); createDisplays(); registerListeners(); load(); unlockAtStart(); initializeCaches(); initializeStates(); updateAll(); } function initializeNews() { news.forEach(entry => { newsWeightFactors.push(0); }); } const cache = {}; function initializeCaches() { const buildingButtons = {}; for (const [key, value] of Object.entries(belongings)) { let button = document.querySelector("#building-" + key); let name = document.querySelector("#building-" + key + " > .building-button-name"); let cost = document.querySelector("#building-" + key + " > .building-button-cost"); buildingButtons[key] = { button: button, name: name, cost: cost } } cache.buildingButtons = buildingButtons; const upgradeButtons = {}; Object.keys(upgrades).forEach(key => { upgradeButtons[key] = document.querySelector("#upgrade-" + key); }); cache.upgradeButtons = upgradeButtons; const resourceLabels = {}; Object.keys(resourceTypes).forEach(key => { resourceLabels[key] = { quantity: document.querySelector("#resource-quantity-" + key), } if (resourceTypes[key].generated) resourceLabels[key].rate = document.querySelector("#resource-rate-" + key); }); cache.resourceLabels = resourceLabels; const optionButtons = {}; optionButtons.numbers = document.querySelector("#numbers"); cache.optionButtons = optionButtons; } const states = {}; // we can keep track of some things, like whether // specific upgrades are currently visible. this // way, we don't have to set them visible every tick; // we can just check if they've been handled already function initializeStates() { initializeBuildingStates(); initializeUpgradeStates(); } function initializeBuildingStates() { const buildingStates = {}; Object.keys(buildings).forEach(key => { buildingStates[key] = { visible: undefined, available: undefined, name: undefined, cost: undefined } }); states.buildings = buildingStates; } function initializeUpgradeStates() { const upgradeStates = {}; Object.keys(upgrades).forEach(key => { upgradeStates[key] = { visible: undefined, available: undefined } }); states.upgrades = upgradeStates; } function unlockAtStart() { unlockBuilding("micro"); for (const [key, value] of Object.entries(belongings)) { if (belongings[key].visible) { unlockBuilding(key); } } } function unlockBuilding(id) { belongings[id].visible = true; document.querySelector("#building-" + id).classList.remove("hidden"); } function initializeData() { for (const [key, value] of Object.entries(buildings)) { belongings[key] = {}; belongings[key].count = 0; belongings[key].visible = false; contributions[key] = makeCost(); } for (const [key, value] of Object.entries(resourceTypes)) { resources[key] = 0; currentProductivity[key] = 0; } for (const [id, upgrade] of Object.entries(upgrades)) { ownedUpgrades[id] = false; for (let effect of upgrade.effects) { if (effects[effect.type] === undefined) { effects[effect.type] = []; } // copy the data and add an entry for the upgrade id that owns the effect let newEffect = {}; for (const [key, value] of Object.entries(effect)) { newEffect[key] = value; } newEffect.parent = id; // unfortunate name collision here // I'm using apply() to pass on any number of arguments to the // apply() function of the effect type newEffect.apply = function (...args) { return effect_types[effect.type].apply.apply(null, [effect].concat(args)); } effects[effect.type].push(newEffect); } } Object.keys(powerups).filter(x => powerups[x].duration !== undefined).forEach(key => activePowerups[key] = { life: 0 }); Object.entries(statTypes).forEach(([key, info]) => { stats[key] = 0; }); } function handleButton(e) { const add = eatPrey(); const text = "+" + render(round(add, 1), 3) + " food"; const gulp = "*glp*"; clickPopup(text, "food", [e.clientX, e.clientY]); clickPopup(gulp, "gulp", [e.clientX, e.clientY]); stats.clicks += 1; } function registerListeners() { document.addEventListener("mouseup", (e) => { if (buttonClicked) { buttonClicked = false; handleButton(e); return false; } else { return true; } }); document.querySelector("#tasty-micro").addEventListener("mousedown", (e) => { buttonClicked = true; }); document.querySelector("#save").addEventListener("click", save); document.querySelector("#reset").addEventListener("click", reset); document.querySelector("#numbers").addEventListener("click", cycleNumbers); document.querySelector("#stats").addEventListener("click", () => document.querySelector("#stats-modal").classList.add("modal-active")); document.querySelector("#options").addEventListener("click", openOptions); document.querySelector("#stats-exit").addEventListener("click", () => document.querySelector("#stats-modal").classList.remove("modal-active")); document.querySelector("#options-exit").addEventListener("click", closeOptions); document.querySelector("#upgrades").addEventListener("click", switchShowOwnedUpgrades); document.addEventListener("keydown", e => { shiftHeld = e.shiftKey; controlHeld = e.ctrlKey; if (mouseTarget) mouseTarget.dispatchEvent(new Event("mousemove")); return true; }); document.addEventListener("keyup", e => { shiftHeld = e.shiftKey; controlHeld = e.ctrlKey; if (mouseTarget) mouseTarget.dispatchEvent(new Event("mousemove")); return true; }); document.querySelectorAll(".help-tooltip").forEach(element => { element.addEventListener("mousemove", e => helpTooltip(element, e)); element.addEventListener("mouseleave", e => helpTooltipRemove(element, e)); }); } function openOptions() { document.querySelector("#options-modal").classList.add("modal-active"); Object.keys(options).forEach(key => { const input = document.querySelector("#option-value-" + key); input.value = options[key].get(); }); } function closeOptions() { document.querySelector("#options-modal").classList.remove("modal-active"); Object.keys(options).forEach(key => { const input = document.querySelector("#option-value-" + key); options[key].set(input.value); }); } function createButtons() { createBuildings(); createUpgrades(); } function createBuildings() { let container = document.querySelector("#buildings-list"); for (const [key, value] of Object.entries(buildings)) { let button = document.createElement("div"); button.classList.add("building-button"); button.classList.add("hidden"); button.id = "building-" + key; let buttonName = document.createElement("div"); buttonName.classList.add("building-button-name"); let buttonCost = document.createElement("div"); buttonCost.classList.add("building-button-cost"); let buildingIcon = document.createElement("i"); buildingIcon.classList.add("fas"); buildingIcon.classList.add(value.icon); button.appendChild(buttonName); button.appendChild(buttonCost); button.appendChild(buildingIcon); button.addEventListener("mousemove", function (e) { mouseTarget = button; buildingTooltip(key, e); }); button.addEventListener("mouseleave", function () { mouseTarget = undefined; buildingTooltipRemove(); }); button.addEventListener("click", function (e) { buyBuilding(key, e); }); button.addEventListener("click", function (e) { buildingTooltip(key, e); }); container.appendChild(button); } } // do we have previous techs and at least one of each building? function upgradeReachable(id) { if (ownedUpgrades[id]) { return false; } if (upgrades[id].prereqs !== undefined) { for (const [type, reqs] of Object.entries(upgrades[id].prereqs)) { if (type == "buildings") { for (const [building, amount] of Object.entries(reqs)) { if (belongings[building].count == 0) { return false; } } } else if (type == "upgrades") { for (let upgrade of reqs) { if (!ownedUpgrades[upgrade]) { return false; } } } else if (type == "resources") { for (const [resource, amount] of Object.entries(reqs)) { if (resources[resource] < amount) { return false; } }; } else if (type == "stats") { for (const [stat, amount] of Object.entries(reqs)) { if (stats[stat] < amount) { return false; } }; } } } return true; } function upgradeAvailable(id) { if (!upgradeReachable(id)) { return false; } if (!canAfford(upgrades[id].cost)) { return false; } if (upgrades[id].prereqs !== undefined) { for (const [type, reqs] of Object.entries(upgrades[id].prereqs)) { if (type == "buildings") { for (const [building, amount] of Object.entries(upgrades[id].prereqs[type])) { if (belongings[building].count < amount) { return false; } } } else if (type == "productivity") { for (const [key, value] of Object.entries(reqs)) { if (currentProductivity[key] < value) { return false; } } } } } return true; } function createUpgrades() { let container = document.querySelector("#upgrades-list"); for (const [key, value] of Object.entries(upgrades)) { remainingUpgrades.push(key); let button = document.createElement("div"); button.classList.add("upgrade-button"); button.classList.add("hidden"); button.id = "upgrade-" + key; const holder = document.createElement("div"); holder.classList.add("upgrade-icon-holder"); button.appendChild(holder); if (typeof(value.icon) == "object") { value.icon.forEach(icon => { let upgradeIcon = document.createElement("i"); upgradeIcon.classList.add("fas"); upgradeIcon.classList.add(icon.icon); upgradeIcon.style.color = icon.color; holder.appendChild(upgradeIcon); if (icon.transform) { upgradeIcon.style.transform = icon.transform; } }) } else { let upgradeIcon = document.createElement("i"); upgradeIcon.classList.add("fas"); upgradeIcon.classList.add(value.icon); holder.appendChild(upgradeIcon); } button.addEventListener("mouseenter", function (e) { mouseTarget = button; upgradeTooltip(key, e); }); button.addEventListener("mousemove", function (e) { mouseTarget = button; upgradeTooltip(key, e); }); button.addEventListener("mouseleave", function () { mouseTarget = undefined; upgradeTooltipRemove(); }); button.addEventListener("click", function (e) { buyUpgrade(key, e); }); container.appendChild(button); } } function createDisplays() { const resourceList = document.querySelector("#resource-list"); Object.keys(resourceTypes).forEach(key => { const quantity = document.createElement("div"); quantity.classList.add("resource-quantity"); quantity.classList.add("help-tooltip"); quantity.dataset.tooltip = resourceTypes[key].desc; quantity.id = "resource-quantity-" + key; resourceList.appendChild(quantity); if (resourceTypes[key].generated) { const rate = document.createElement("div"); rate.classList.add("resource-rate"); rate.id = "resource-rate-" + key; resourceList.appendChild(rate); } }) const statHolder = document.querySelector("#stats-holder"); Object.keys(statTypes).forEach(key => { const div = document.createElement("div"); div.classList.add("stat-line"); const name = document.createElement("div"); name.classList.add("stat-name"); const value = document.createElement("div"); value.classList.add("stat-value"); value.id = "stat-value-" + key; name.innerText = statTypes[key].name; value.innerText = stats[key]; div.appendChild(name); div.appendChild(value); statHolder.append(div); }); const optionHolder = document.querySelector("#options-holder"); Object.keys(options).forEach(key => { const div = document.createElement("div"); div.classList.add("option-line"); const name = document.createElement("div"); name.classList.add("option-name"); const value = document.createElement("input"); value.classList.add("option-value"); value.id = "option-value-" + key; name.innerText = options[key].name; value.innerText = options[key].get(); div.appendChild(name); div.appendChild(value); optionHolder.append(div); }); } function renderLine(line) { let div = document.createElement("div"); div.innerText = line.text; if (line.valid !== undefined) { if (line.valid) { div.classList.add("cost-met"); } else { div.classList.add("cost-unmet"); } } if (line.class !== undefined) { for (let entry of line.class.split(",")) { div.classList.add(entry); } } return div; } function renderLines(lines) { let divs = []; for (let line of lines) { divs.push(renderLine(line)); } return divs; } function renderCost(cost) { let list = []; list.push({ "text": "Cost:" }); for (const [key, value] of Object.entries(cost)) { list.push({ "text": render(value, 0) + " " + resourceTypes[key].name, "valid": resources[key] >= value }); } return renderLines(list); } function renderPrereqs(prereqs) { let list = []; if (prereqs === undefined) { return renderLines(list); } list.push({ "text": "Own:" }); for (const [key, value] of Object.entries(prereqs)) { if (key == "buildings") { for (const [id, amount] of Object.entries(prereqs.buildings)) { list.push({ "text": buildings[id].name + " x" + render(amount, 0), "valid": belongings[id].count >= amount }); } } else if (key == "productivity") { for (const [id, amount] of Object.entries(prereqs.productivity)) { list.push({ "text": render(amount, 0) + " " + resourceTypes[id].name + "/s", "valid": currentProductivity[id] >= amount }); } } } return renderLines(list); } function renderEffects(effectList) { let list = []; for (let effect of effectList) { list.push({ "text": effect_types[effect.type].desc(effect) }); } return renderLines(list); } function clickPopup(text, type, location) { const div = document.createElement("div"); div.textContent = text; div.classList.add("click-popup-" + type); var direction; if (type == "food") { direction = -150; } else if (type == "gulp") { direction = -150; } else if (type == "upgrade") { direction = -50; } else if (type == "info") { direction = 0; } direction *= Math.random() * 0.5 + 1; direction = Math.round(direction) + "px" div.style.setProperty("--target", direction) div.style.left = location[0] + "px"; div.style.top = location[1] + "px"; const body = document.querySelector("body"); body.appendChild(div); setTimeout(() => { body.removeChild(div); }, 2000); } function doNews() { let options = []; let weights = []; let indices = []; let index = 0; news.forEach(entry => { if (entry.condition(state) && newsWeightFactors[index] != 1) { options = options.concat(entry.lines); weights.push(1 - newsWeightFactors[index]) indices.push(index); } index += 1; }); const choice = weightedSelect(weights); showNews(options[choice](state)); for (let i = 0; i < newsWeightFactors.length; i++) { newsWeightFactors[i] *= 0.9; } newsWeightFactors[indices[choice]] = 1; newsShowTimer = setTimeout(() => { doNews(); }, 8000); } function showNews(text) { const div = document.createElement("div"); div.innerHTML = text; div.classList.add("news-text"); const body = document.querySelector("body"); div.addEventListener("click", () => { clearTimeout(newsShowTimer); clearTimeout(newsRemoveTimer); div.classList.add("news-text-leaving"); setTimeout(() => { body.removeChild(div); }, 1000); doNews(); }); body.appendChild(div); newsRemoveTimer = setTimeout(() => { div.classList.add("news-text-leaving"); setTimeout(() => { body.removeChild(div); }, 1000); }, 8000); } function doPowerup() { const lifetime = 10000; const button = document.createElement("div"); const left = Math.round(Math.random() * 50 + 25) + "%"; const top = Math.round(Math.random() * 50 + 25) + "%"; button.classList.add("powerup"); button.style.setProperty("--lifetime", lifetime / 1000 + "s"); button.style.setProperty("--leftpos", left); button.style.setProperty("--toppos", top); const body = document.querySelector("body"); body.appendChild(button); const choices = []; Object.entries(powerups).forEach(([key, val]) => { if (val.prereqs(state)) choices.push(key); }); const choice = Math.floor(Math.random() * choices.length); const powerup = powerups[choices[choice]]; const icon = document.createElement("div"); icon.classList.add("fas"); icon.classList.add(powerup.icon); button.appendChild(icon); const remove = setTimeout(() => { body.removeChild(button); }, lifetime); let delay = 60000 + Math.random() * 30000; for (let effect of effects["powerup-freq"]) { if (ownedUpgrades[effect.parent]) { delay = effect.apply(delay); } } setTimeout(() => { doPowerup(); }, delay); button.addEventListener("mousedown", e => { if (powerup.duration !== undefined) { addPowerup(choices[choice], powerup); } else { powerup.effect(state); } powerup.popup(powerup, e); button.classList.add("powerup-clicked"); resources.powerups += 1; clearTimeout(remove); stats.powerups += 1; setTimeout(() => { body.removeChild(button); }, 500); }); } function fillTooltip(type, field, content) { let item = document.querySelector("#" + type + "-tooltip-" + field); if (typeof (content) === "string") { item.innerText = content; } else { replaceChildren(item, content); } } function upgradeTooltip(id, event) { let tooltip = document.querySelector("#upgrade-tooltip"); tooltip.style.setProperty("display", "inline-block"); fillTooltip("upgrade", "name", upgrades[id].name); fillTooltip("upgrade", "desc", upgrades[id].desc); fillTooltip("upgrade", "effect", renderEffects(upgrades[id].effects)); fillTooltip("upgrade", "cost", renderCost(upgrades[id].cost)); fillTooltip("upgrade", "prereqs", renderPrereqs(upgrades[id].prereqs)); let yOffset = tooltip.parentElement.getBoundingClientRect().y; let tooltipSize = tooltip.getBoundingClientRect().height; let yTrans = Math.round(event.clientY - yOffset); var body = document.body, html = document.documentElement; var height = Math.max(window.innerHeight); yTrans = Math.min(yTrans, height - tooltipSize - 150); tooltip.style.setProperty("transform", "translate(-420px, " + yTrans + "px)"); } function upgradeTooltipRemove() { let tooltip = document.querySelector("#upgrade-tooltip"); tooltip.style.setProperty("display", "none"); } function prodSummary(id) { let list = []; list.push( { "text": "Each " + buildings[id].name + " produces " + render(belongings[id].count == 0 ? 0 : contributions[id].food / belongings[id].count, 3) + " food/sec" } ); list.push( { "text": "Your " + render(belongings[id].count) + " " + (belongings[id].count == 1 ? buildings[id].name + " is" : buildings[id].plural + " are") + " producing " + render(contributions[id].food, 3) + " food/sec" } ); let percentage = round(100 * contributions[id].food / currentProductivity["food"], 2); if (isNaN(percentage)) { percentage = 0; } list.push( { "text": "(" + percentage + "% of all food)" } ); return renderLines(list); } function buildingTooltip(id, event) { let tooltip = document.querySelector("#building-tooltip"); tooltip.style.setProperty("display", "inline-block"); const count = buildingCount(); fillTooltip("building", "name", (count != 1 ? count + "x " : "") + buildings[id].name); fillTooltip("building", "desc", buildings[id].desc); fillTooltip("building", "cost", render(costOfBuilding(id, count).food) + " food"); fillTooltip("building", "prod", prodSummary(id)); let xPos = tooltip.parentElement.getBoundingClientRect().x - 450; // wow browsers are bad var body = document.body, html = document.documentElement; var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); let yPos = Math.min(event.clientY, height - 200); tooltip.style.setProperty("transform", "translate(" + xPos + "px, " + yPos + "px)") } function buildingTooltipRemove() { let tooltip = document.querySelector("#building-tooltip"); tooltip.style.setProperty("display", "none"); } function powerupTooltip(id, event) { let tooltip = document.querySelector("#powerup-tooltip"); tooltip.style.setProperty("display", "inline-block"); fillTooltip("powerup", "name", powerups[id].name); fillTooltip("powerup", "desc", powerups[id].description); let xPos = tooltip.parentElement.getBoundingClientRect().x + 100; // wow browsers are bad var body = document.body, html = document.documentElement; var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); let yPos = Math.min(event.clientY - 100, height - 150); tooltip.style.setProperty("transform", "translate(" + xPos + "px, " + yPos + "px)") } function powerupTooltipRemove() { let tooltip = document.querySelector("#powerup-tooltip"); tooltip.style.setProperty("display", "none"); } function helpTooltip(element, event) { const text = element.dataset.tooltip; let tooltip = document.querySelector("#help-tooltip"); tooltip.style.setProperty("display", "inline-block"); const count = buildingCount(); fillTooltip("help", "help", text); let xPos = event.clientX + 100; // wow browsers are bad var body = document.body, html = document.documentElement; var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); let yPos = Math.min(event.clientY, height - 200); tooltip.style.setProperty("transform", "translate(" + xPos + "px, " + yPos + "px)") } function helpTooltipRemove() { let tooltip = document.querySelector("#help-tooltip"); tooltip.style.setProperty("display", "none"); } window.onload = function () { setup(); lastTime = performance.now(); doNews(); doPowerup(); setTimeout(updateDisplay, 1000 / updateRate); setTimeout(autosave, 60000); } window.onblur = function() { controlHeld = false; shiftHeld = false; } window.onfocus = function() { window.dispatchEvent(new Event("keydown")) } function autosave() { saveGame(); let x = window.innerWidth / 2; let y = window.innerHeight * 9 / 10; clickPopup("Autosaving...", "info", [x, y]); setTimeout(autosave, 60000); } function save(e) { saveGame(); clickPopup("Saved!", "info", [e.clientX, e.clientY]); } function saveGame() { try { let storage = window.localStorage; const save = {} save.version = migrations.length; save.ownedUpgrades = ownedUpgrades; save.resources = resources; save.belongings = belongings; save.stats = stats; save.macroDesc = macroDesc; storage.setItem("save", JSON.stringify(save)); } catch (e) { clickPopup("Can't save - no access to local storage.", "info", [window.innerWidth / 2, window.innerHeight / 5]); } } const migrations = [ // dummy migration, because there was no version 0 save => { }, // introduce stats save => { save.stats = {} }, // introduce macroDesc save => { save.macroDesc = {} } ] function migrate(save) { let version = save.version; while (version != migrations.length) { migrations[version](save); version += 1; } save.version = version; } function load() { try { let storage = window.localStorage; // migrate to everything in one if (storage.getItem("save-version") !== null) { const save = {}; save.ownedUpgrades = JSON.parse(storage.getItem("ownedUpgrades")); save.resources = JSON.parse(storage.getItem("resources")); save.belongings = JSON.parse(storage.getItem("belongings")); save.version = 1; storage.clear(); storage.setItem("save", JSON.stringify(save)) } const save = JSON.parse(storage.getItem("save")); if (save == null) return; migrate(save); for (const [key, value] of Object.entries(save.ownedUpgrades)) { ownedUpgrades[key] = value; } for (const [key, value] of Object.entries(save.resources)) { resources[key] = value; } for (const [key, value] of Object.entries(save.belongings)) { belongings[key] = value; } for (const [key, value] of Object.entries(save.stats)) { stats[key] = value; } for (const [key, value] of Object.entries(save.macroDesc)) { macroDesc[key] = value; } } catch (e) { console.error(e); clickPopup("Can't load - no access to local storage.", "info", [window.innerWidth / 2, window.innerHeight / 5]); } } function reset() { window.localStorage.clear(); } function cycleNumbers() { numberMode = numberModes[numberMode.next]; updateOptions(); }