cookie clicker but bigger
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1563 line
38 KiB

  1. "use strict";
  2. const belongings = {};
  3. const stats = {};
  4. const macroDesc = {
  5. name: "Fen",
  6. species: "crux",
  7. proSubject: "he",
  8. proObject: "him",
  9. proPossessive: "his",
  10. }
  11. const ownedUpgrades = {};
  12. const remainingUpgrades = [];
  13. let showOwnedUpgrades = false;
  14. const effects = {};
  15. const resources = {};
  16. let updateRate = 60;
  17. const currentProductivity = {};
  18. const contributions = {};
  19. const clickPowers = {
  20. clickBonus: 0,
  21. clickMultiplier: 1,
  22. clickVictim: "micro",
  23. clickSeconds: 10
  24. }
  25. let clickBonus = 0;
  26. let clickVictim = "micro";
  27. let lastTime = 0;
  28. let controlHeld = false;
  29. let shiftHeld = false;
  30. let mouseTarget = undefined;
  31. let newsShowTimer;
  32. let newsRemoveTimer;
  33. const newsDelay = 8000;
  34. const newsWeightFactors = [];
  35. let buttonClicked = false;
  36. const state = {
  37. ownedUpgrades: ownedUpgrades,
  38. resources: resources,
  39. currentProductivity: currentProductivity,
  40. belongings: belongings,
  41. clickPowers: clickPowers,
  42. stats: stats
  43. };
  44. const numberModes = {
  45. words: {
  46. name: "Words",
  47. render: numberText,
  48. next: "smallWords"
  49. },
  50. smallWords: {
  51. name: "Small Words",
  52. render: numberTextSmall,
  53. next: "scientific"
  54. },
  55. scientific: {
  56. name: "Scientific",
  57. render: numberScientific,
  58. next: "full",
  59. },
  60. full: {
  61. name: "Full",
  62. render: numberFull,
  63. next: "words"
  64. }
  65. }
  66. deepFreeze(numberModes);
  67. let numberMode = numberModes["words"];
  68. const activePowerups = {};
  69. function tickPowerups(delta) {
  70. const powerupList = document.querySelector("#powerup-list");
  71. let changed = false;
  72. // I love mutating arrays as I traverse them.
  73. Object.entries(activePowerups).filter(x => x[1].life > 0).forEach(([key, data]) => {
  74. const newLife = data.life - delta;
  75. if (newLife <= 0) {
  76. setTimeout(() => {
  77. powerupList.removeChild(data.element);
  78. }, 1000);
  79. data.element.classList.add("powerup-entry-done");
  80. activePowerups[key].life = 0;
  81. changed = true;
  82. } else {
  83. data.life = newLife;
  84. const frac = (data.maxLife - data.life) / (data.maxLife);
  85. data.element.style.setProperty("--progress", frac * 100 + "%")
  86. }
  87. });
  88. if (changed) {
  89. updateAll();
  90. }
  91. }
  92. function addPowerup(key, powerup) {
  93. // powerup already exists
  94. if (activePowerups[key].life > 0) {
  95. activePowerups[key].life += powerup.duration;
  96. activePowerups[key].maxLife = activePowerups[key].life;
  97. } else {
  98. const powerupList = document.querySelector("#powerup-list");
  99. const powerupEntry = document.createElement("div");
  100. powerupEntry.classList.add("powerup-entry");
  101. const powerupIconHolder = document.createElement("div");
  102. powerupIconHolder.classList.add("powerup-entry-icon-holder");
  103. const powerupIcon = document.createElement("i");
  104. powerupIcon.classList.add("fas");
  105. powerupIcon.classList.add(powerup.icon);
  106. powerupIconHolder.appendChild(powerupIcon);
  107. powerupEntry.appendChild(powerupIconHolder);
  108. powerupList.appendChild(powerupEntry);
  109. activePowerups[key] = {powerup: powerup, life: powerup.duration, maxLife: powerup.duration, element: powerupEntry };
  110. powerupEntry.addEventListener("mousemove", function (e) { powerupTooltip(key, e); });
  111. powerupEntry.addEventListener("mouseleave", function () { powerupTooltipRemove(); });
  112. updateAll();
  113. }
  114. }
  115. function applyGlobalProdBonus(cost) {
  116. for (let effect of effects["prod-all"]) {
  117. if (ownedUpgrades[effect.parent]) {
  118. effect.apply(cost);
  119. }
  120. }
  121. }
  122. function calculateProductivity() {
  123. let productivity = makeCost();
  124. for (const [key, value] of Object.entries(belongings)) {
  125. const provided = productivityOf(key);
  126. productivity = addCost(productivity, provided);
  127. contributions[key] = provided;
  128. }
  129. return productivity;
  130. }
  131. // here's where upgrades will go :3
  132. function applyProductivityMultipliers(type, cost) {
  133. for (let effect of effects["prod"]) {
  134. if (ownedUpgrades[effect.parent] && effect.target == type) {
  135. effect.apply(cost);
  136. }
  137. }
  138. for (let effect of effects["helper"]) {
  139. if (ownedUpgrades[effect.parent] && effect.helped == type) {
  140. effect.apply(cost, belongings[effect.helper].count);
  141. }
  142. }
  143. }
  144. function productivityOf(type) {
  145. let baseProd = makeCost(buildings[type].prod);
  146. applyProductivityMultipliers(type, baseProd);
  147. applyGlobalProdBonus(baseProd);
  148. scaleCost(baseProd, belongings[type].count);
  149. return baseProd;
  150. }
  151. function makeCost(source) {
  152. const empty = mapObject(resourceTypes, () => 0);
  153. return {...empty, ...source};
  154. }
  155. function addCost(cost1, cost2) {
  156. return Object.keys(resourceTypes).reduce((o, k) => {o[k] += cost2[k]; return o;}, cost1);
  157. }
  158. function scaleCost(cost, scale) {
  159. return Object.keys(resourceTypes).reduce((o, k) => {o[k] *= scale; return o;}, cost);
  160. }
  161. function costOfBuilding(type, count = 1) {
  162. let total = makeCost();
  163. while (count > 0) {
  164. let baseCost = makeCost(buildings[type].cost);
  165. baseCost = scaleCost(baseCost, Math.pow(1.15, belongings[type].count + count - 1));
  166. total = addCost(total, baseCost);
  167. count--;
  168. }
  169. return mapObject(total, round);
  170. }
  171. function buildingCount() {
  172. if (controlHeld) {
  173. return 10;
  174. } else if (shiftHeld) {
  175. return 100;
  176. } else {
  177. return 1;
  178. }
  179. }
  180. function buyBuilding(type, e) {
  181. const count = buildingCount();
  182. let cost = costOfBuilding(type, count);
  183. if (canAfford(cost)) {
  184. spend(cost);
  185. belongings[type].count += count;
  186. }
  187. updateProductivity();
  188. }
  189. function updateAll() {
  190. updateProductivity();
  191. updateClickVictim();
  192. updateOptions();
  193. }
  194. function updateOptions() {
  195. cache.optionButtons.numbers.innerText = "Number mode: " + numberMode.name;
  196. }
  197. // update stuff
  198. function updateDisplay() {
  199. let newTime = performance.now();
  200. let delta = newTime - lastTime;
  201. lastTime = newTime;
  202. addResources(delta);
  203. displayResources();
  204. displayBuildings();
  205. displayUpgrades(showOwnedUpgrades);
  206. tickPowerups(delta);
  207. Object.keys(statTypes).forEach(key => {
  208. const value = document.querySelector("#stat-value-" + key);
  209. value.innerText = render(stats[key]);
  210. })
  211. stats.seconds += delta / 1000;
  212. setTimeout(updateDisplay, 1000 / updateRate);
  213. }
  214. function updateProductivity() {
  215. Object.assign(currentProductivity, calculateProductivity());
  216. // maybe this should go somewhere else - it also does clicking...
  217. updateClickPowers();
  218. Object.entries(activePowerups).forEach(([key, entry]) => {
  219. if (entry.life > 0) {
  220. const powerup = entry.powerup;
  221. powerup.effect(state);
  222. }
  223. });
  224. }
  225. function addResources(delta) {
  226. for (const [resource, amount] of Object.entries(currentProductivity)) {
  227. const gained = amount * delta / 1000;
  228. resources[resource] += gained;
  229. if (resource == "food")
  230. stats.food += gained;
  231. }
  232. }
  233. function displayResources() {
  234. document.title = "Gorge - " + round(resources.food) + " food";
  235. Object.keys(resources).forEach(key => {
  236. cache.resourceLabels[key].quantity.innerText = render(resources[key]) + " " + resourceTypes[key].name;
  237. if (resourceTypes[key].generated)
  238. cache.resourceLabels[key].rate.innerText = render(currentProductivity[key]) + " " + resourceTypes[key].name + "/sec";
  239. })
  240. }
  241. function displayBuildings() {
  242. const count = buildingCount();
  243. for (const [key, value] of Object.entries(belongings)) {
  244. let available = states.buildings[key].available;
  245. if (!belongings[key].visible) {
  246. if (resources.food * 10 >= costOfBuilding(key).food) {
  247. unlockBuilding(key);
  248. } if (belongings[key].count > 0) {
  249. unlockBuilding(key);
  250. } else {
  251. continue;
  252. }
  253. belongings[key].visible = true;
  254. let button = cache.buildingButtons[key].button;
  255. button.classList.remove("hidden");
  256. }
  257. let button = cache.buildingButtons[key].button;
  258. let name = cache.buildingButtons[key].name;
  259. let cost = cache.buildingButtons[key].cost;
  260. const buildingCost = costOfBuilding(key, count);
  261. const newName = value.count + " " + (value.count == 1 ? buildings[key].name : buildings[key].plural);
  262. if (newName != states.buildings[key].name) {
  263. name.innerText = newName;
  264. states.buildings[key].name = newName;
  265. }
  266. const newCost = render(buildingCost.food) + " food";
  267. if (newCost != states.buildings[key].cost) {
  268. cost.innerText = newCost;
  269. states.buildings[key].cost = newCost;
  270. }
  271. if (canAfford(buildingCost) && available !== true) {
  272. button.classList.remove("building-button-disabled");
  273. cost.classList.add("building-button-cost-valid");
  274. states.buildings[key].available = true;
  275. } else if (!canAfford(buildingCost) && available !== false) {
  276. button.classList.add("building-button-disabled");
  277. cost.classList.add("building-button-cost-invalid");
  278. states.buildings[key].available = false;
  279. }
  280. }
  281. }
  282. function canAfford(cost) {
  283. for (const [resource, amount] of Object.entries(cost)) {
  284. if (resources[resource] < amount) {
  285. return false;
  286. }
  287. }
  288. return true;
  289. }
  290. function spend(cost) {
  291. for (const [resource, amount] of Object.entries(cost)) {
  292. resources[resource] -= amount;
  293. }
  294. }
  295. function switchShowOwnedUpgrades() {
  296. initializeUpgradeStates();
  297. if (showOwnedUpgrades) {
  298. document.querySelector("#upgrades").innerText = "Upgrades";
  299. } else {
  300. document.querySelector("#upgrades").innerText = "Owned Upgrades";
  301. }
  302. showOwnedUpgrades = !showOwnedUpgrades;
  303. }
  304. function displayUpgrades(owned) {
  305. if (owned) {
  306. Object.entries(ownedUpgrades).forEach(([key, val]) => {
  307. let button = cache.upgradeButtons[key];
  308. if (val) {
  309. button.classList.remove("hidden");
  310. } else {
  311. button.classList.add("hidden");
  312. }
  313. });
  314. }
  315. else {
  316. for (let id of remainingUpgrades) {
  317. let button = cache.upgradeButtons[id];
  318. let visible = states.upgrades[id].visible;
  319. let available = states.upgrades[id].available;
  320. if (ownedUpgrades[id] && visible !== false) {
  321. button.classList.add("hidden");
  322. states.upgrades[id].visible = false;
  323. continue;
  324. }
  325. if (upgradeReachable(id) && visible !== true) {
  326. button.classList.remove("hidden");
  327. states.upgrades[id].visible = true;
  328. } else if (!upgradeReachable(id) && visible !== false) {
  329. button.classList.add("hidden");
  330. states.upgrades[id].visible = false;
  331. }
  332. if (upgradeAvailable(id) && available !== true) {
  333. button.classList.remove("upgrade-button-inactive");
  334. states.upgrades[id].available = true;
  335. } else if (!upgradeAvailable(id) && available !== false) {
  336. button.classList.add("upgrade-button-inactive");
  337. states.upgrades[id].available = false;
  338. }
  339. }
  340. // we aren't trimming the list of upgrades now
  341. // because we need to switch between owned and unowned upgrades
  342. // - thus we need to be able to show or hide anything
  343. /*
  344. for (let i = remainingUpgrades.length-1; i >= 0; i--) {
  345. if (ownedUpgrades[remainingUpgrades[i]]) {
  346. remainingUpgrades.splice(i, 1);
  347. }
  348. }*/
  349. }
  350. }
  351. function updateClickPowers() {
  352. let bonus = 0;
  353. clickPowers.clickMultiplier = 1;
  354. for (let effect of effects["click"]) {
  355. if (ownedUpgrades[effect.parent]) {
  356. bonus = effect.apply(bonus, currentProductivity["food"]);
  357. }
  358. }
  359. clickPowers.clickBonus = bonus;
  360. }
  361. function updateClickVictim() {
  362. const button = document.querySelector("#tasty-micro");
  363. button.classList.remove(...button.classList);
  364. for (let i=effects["click-victim"].length - 1; i >=0; i--) {
  365. const effect = effects["click-victim"][i];
  366. if (ownedUpgrades[effect.parent]) {
  367. clickPowers.clickVictim = effect.id;
  368. button.classList.add("fas")
  369. button.classList.add(buildings[effect.id].icon)
  370. return;
  371. }
  372. }
  373. clickPowers.clickVictim = "micro";
  374. button.classList.add("fas")
  375. button.classList.add(buildings.micro.icon)
  376. }
  377. function buyUpgrade(id, e) {
  378. if (ownedUpgrades[id]) {
  379. return;
  380. }
  381. let upgrade = upgrades[id];
  382. if (!upgradeAvailable(id)) {
  383. return;
  384. }
  385. spend(upgrade.cost);
  386. ownedUpgrades[id] = true;
  387. let text = "Bought " + upgrade.name + "!";
  388. clickPopup(text, "upgrade", [e.clientX, e.clientY]);
  389. updateProductivity();
  390. updateClickVictim();
  391. }
  392. function eatPrey() {
  393. const add = clickPowers.clickMultiplier * (buildings[clickPowers.clickVictim]["prod"].food * clickPowers.clickSeconds + clickPowers.clickBonus);
  394. resources.food += add;
  395. stats.foodClicked += add;
  396. return add;
  397. }
  398. // setup stuff lol
  399. // we'll initialize the dict of buildings we can own
  400. function setup() {
  401. // create static data
  402. createTemplateUpgrades();
  403. // prepare dynamic stuff
  404. initializeData();
  405. initializeNews();
  406. createButtons();
  407. createDisplays();
  408. registerListeners();
  409. load();
  410. unlockAtStart();
  411. initializeCaches();
  412. initializeStates();
  413. updateAll();
  414. }
  415. function initializeNews() {
  416. news.forEach(entry => {
  417. newsWeightFactors.push(0);
  418. });
  419. }
  420. const cache = {};
  421. function initializeCaches() {
  422. const buildingButtons = {};
  423. for (const [key, value] of Object.entries(belongings)) {
  424. let button = document.querySelector("#building-" + key);
  425. let name = document.querySelector("#building-" + key + " > .building-button-name");
  426. let cost = document.querySelector("#building-" + key + " > .building-button-cost");
  427. buildingButtons[key] = {
  428. button: button,
  429. name: name,
  430. cost: cost
  431. }
  432. }
  433. cache.buildingButtons = buildingButtons;
  434. const upgradeButtons = {};
  435. Object.keys(upgrades).forEach(key => {
  436. upgradeButtons[key] = document.querySelector("#upgrade-" + key);
  437. });
  438. cache.upgradeButtons = upgradeButtons;
  439. const resourceLabels = {};
  440. Object.keys(resourceTypes).forEach(key => {
  441. resourceLabels[key] = {
  442. quantity: document.querySelector("#resource-quantity-" + key),
  443. }
  444. if (resourceTypes[key].generated)
  445. resourceLabels[key].rate = document.querySelector("#resource-rate-" + key);
  446. });
  447. cache.resourceLabels = resourceLabels;
  448. const optionButtons = {};
  449. optionButtons.numbers = document.querySelector("#numbers");
  450. cache.optionButtons = optionButtons;
  451. }
  452. const states = {};
  453. // we can keep track of some things, like whether
  454. // specific upgrades are currently visible. this
  455. // way, we don't have to set them visible every tick;
  456. // we can just check if they've been handled already
  457. function initializeStates() {
  458. initializeBuildingStates();
  459. initializeUpgradeStates();
  460. }
  461. function initializeBuildingStates() {
  462. const buildingStates = {};
  463. Object.keys(buildings).forEach(key => {
  464. buildingStates[key] = {
  465. visible: undefined,
  466. available: undefined,
  467. name: undefined,
  468. cost: undefined
  469. }
  470. });
  471. states.buildings = buildingStates;
  472. }
  473. function initializeUpgradeStates() {
  474. const upgradeStates = {};
  475. Object.keys(upgrades).forEach(key => {
  476. upgradeStates[key] = {
  477. visible: undefined,
  478. available: undefined
  479. }
  480. });
  481. states.upgrades = upgradeStates;
  482. }
  483. function unlockAtStart() {
  484. unlockBuilding("micro");
  485. for (const [key, value] of Object.entries(belongings)) {
  486. if (belongings[key].visible) {
  487. unlockBuilding(key);
  488. }
  489. }
  490. }
  491. function unlockBuilding(id) {
  492. belongings[id].visible = true;
  493. document.querySelector("#building-" + id).classList.remove("hidden");
  494. }
  495. function initializeData() {
  496. for (const [key, value] of Object.entries(buildings)) {
  497. belongings[key] = {};
  498. belongings[key].count = 0;
  499. belongings[key].visible = false;
  500. contributions[key] = makeCost();
  501. }
  502. for (const [key, value] of Object.entries(resourceTypes)) {
  503. resources[key] = 0;
  504. currentProductivity[key] = 0;
  505. }
  506. for (const [id, upgrade] of Object.entries(upgrades)) {
  507. ownedUpgrades[id] = false;
  508. for (let effect of upgrade.effects) {
  509. if (effects[effect.type] === undefined) {
  510. effects[effect.type] = [];
  511. }
  512. // copy the data and add an entry for the upgrade id that owns the effect
  513. let newEffect = {};
  514. for (const [key, value] of Object.entries(effect)) {
  515. newEffect[key] = value;
  516. }
  517. newEffect.parent = id;
  518. // unfortunate name collision here
  519. // I'm using apply() to pass on any number of arguments to the
  520. // apply() function of the effect type
  521. newEffect.apply = function (...args) { return effect_types[effect.type].apply.apply(null, [effect].concat(args)); }
  522. effects[effect.type].push(newEffect);
  523. }
  524. }
  525. Object.keys(powerups).filter(x => powerups[x].duration !== undefined).forEach(key => activePowerups[key] = {
  526. life: 0
  527. });
  528. Object.entries(statTypes).forEach(([key, info]) => {
  529. stats[key] = 0;
  530. });
  531. }
  532. function handleButton(e) {
  533. const add = eatPrey();
  534. const text = "+" + render(round(add, 1), 3) + " food";
  535. const gulp = "*glp*";
  536. clickPopup(text, "food", [e.clientX, e.clientY]);
  537. clickPopup(gulp, "gulp", [e.clientX, e.clientY]);
  538. stats.clicks += 1;
  539. }
  540. function registerListeners() {
  541. document.addEventListener("mouseup", (e) => {
  542. if (buttonClicked) {
  543. buttonClicked = false;
  544. handleButton(e);
  545. return false;
  546. } else {
  547. return true;
  548. }
  549. });
  550. document.querySelector("#tasty-micro").addEventListener("mousedown", (e) => {
  551. buttonClicked = true;
  552. });
  553. document.querySelector("#save").addEventListener("click", save);
  554. document.querySelector("#reset").addEventListener("click", reset);
  555. document.querySelector("#numbers").addEventListener("click", cycleNumbers);
  556. document.querySelector("#stats").addEventListener("click", () => document.querySelector("#stats-modal").classList.add("modal-active"));
  557. document.querySelector("#options").addEventListener("click", openOptions);
  558. document.querySelector("#stats-exit").addEventListener("click", () => document.querySelector("#stats-modal").classList.remove("modal-active"));
  559. document.querySelector("#options-exit").addEventListener("click", closeOptions);
  560. document.querySelector("#upgrades").addEventListener("click", switchShowOwnedUpgrades);
  561. document.addEventListener("keydown", e => {
  562. shiftHeld = e.shiftKey;
  563. controlHeld = e.ctrlKey;
  564. if (mouseTarget)
  565. mouseTarget.dispatchEvent(new Event("mousemove"));
  566. return true;
  567. });
  568. document.addEventListener("keyup", e => {
  569. shiftHeld = e.shiftKey;
  570. controlHeld = e.ctrlKey;
  571. if (mouseTarget)
  572. mouseTarget.dispatchEvent(new Event("mousemove"));
  573. return true;
  574. });
  575. document.querySelectorAll(".help-tooltip").forEach(element => {
  576. element.addEventListener("mousemove", e => helpTooltip(element, e));
  577. element.addEventListener("mouseleave", e => helpTooltipRemove(element, e));
  578. });
  579. }
  580. function openOptions() {
  581. document.querySelector("#options-modal").classList.add("modal-active");
  582. Object.keys(options).forEach(key => {
  583. const input = document.querySelector("#option-value-" + key);
  584. input.value = options[key].get();
  585. });
  586. }
  587. function closeOptions() {
  588. document.querySelector("#options-modal").classList.remove("modal-active");
  589. Object.keys(options).forEach(key => {
  590. const input = document.querySelector("#option-value-" + key);
  591. options[key].set(input.value);
  592. });
  593. }
  594. function createButtons() {
  595. createBuildings();
  596. createUpgrades();
  597. }
  598. function createBuildings() {
  599. let container = document.querySelector("#buildings-list");
  600. for (const [key, value] of Object.entries(buildings)) {
  601. let button = document.createElement("div");
  602. button.classList.add("building-button");
  603. button.classList.add("hidden");
  604. button.id = "building-" + key;
  605. let buttonName = document.createElement("div");
  606. buttonName.classList.add("building-button-name");
  607. let buttonCost = document.createElement("div");
  608. buttonCost.classList.add("building-button-cost");
  609. let buildingIcon = document.createElement("i");
  610. buildingIcon.classList.add("fas");
  611. buildingIcon.classList.add(value.icon);
  612. button.appendChild(buttonName);
  613. button.appendChild(buttonCost);
  614. button.appendChild(buildingIcon);
  615. button.addEventListener("mousemove", function (e) { mouseTarget = button; buildingTooltip(key, e); });
  616. button.addEventListener("mouseleave", function () { mouseTarget = undefined; buildingTooltipRemove(); });
  617. button.addEventListener("click", function (e) { buyBuilding(key, e); });
  618. button.addEventListener("click", function (e) { buildingTooltip(key, e); });
  619. container.appendChild(button);
  620. }
  621. }
  622. // do we have previous techs and at least one of each building?
  623. function upgradeReachable(id) {
  624. if (ownedUpgrades[id]) {
  625. return false;
  626. }
  627. if (upgrades[id].prereqs !== undefined) {
  628. for (const [type, reqs] of Object.entries(upgrades[id].prereqs)) {
  629. if (type == "buildings") {
  630. for (const [building, amount] of Object.entries(reqs)) {
  631. if (belongings[building].count == 0) {
  632. return false;
  633. }
  634. }
  635. }
  636. else if (type == "upgrades") {
  637. for (let upgrade of reqs) {
  638. if (!ownedUpgrades[upgrade]) {
  639. return false;
  640. }
  641. }
  642. }
  643. else if (type == "resources") {
  644. for (const [resource, amount] of Object.entries(reqs)) {
  645. if (resources[resource] < amount) {
  646. return false;
  647. }
  648. };
  649. }
  650. else if (type == "stats") {
  651. for (const [stat, amount] of Object.entries(reqs)) {
  652. if (stats[stat] < amount) {
  653. return false;
  654. }
  655. };
  656. }
  657. }
  658. }
  659. return true;
  660. }
  661. function upgradeAvailable(id) {
  662. if (!upgradeReachable(id)) {
  663. return false;
  664. }
  665. if (!canAfford(upgrades[id].cost)) {
  666. return false;
  667. }
  668. if (upgrades[id].prereqs !== undefined) {
  669. for (const [type, reqs] of Object.entries(upgrades[id].prereqs)) {
  670. if (type == "buildings") {
  671. for (const [building, amount] of Object.entries(upgrades[id].prereqs[type])) {
  672. if (belongings[building].count < amount) {
  673. return false;
  674. }
  675. }
  676. } else if (type == "productivity") {
  677. for (const [key, value] of Object.entries(reqs)) {
  678. if (currentProductivity[key] < value) {
  679. return false;
  680. }
  681. }
  682. }
  683. }
  684. }
  685. return true;
  686. }
  687. function createUpgrades() {
  688. let container = document.querySelector("#upgrades-list");
  689. for (const [key, value] of Object.entries(upgrades)) {
  690. remainingUpgrades.push(key);
  691. let button = document.createElement("div");
  692. button.classList.add("upgrade-button");
  693. button.classList.add("hidden");
  694. button.id = "upgrade-" + key;
  695. const holder = document.createElement("div");
  696. holder.classList.add("upgrade-icon-holder");
  697. button.appendChild(holder);
  698. if (typeof(value.icon) == "object") {
  699. value.icon.forEach(icon => {
  700. let upgradeIcon = document.createElement("i");
  701. upgradeIcon.classList.add("fas");
  702. upgradeIcon.classList.add(icon.icon);
  703. upgradeIcon.style.color = icon.color;
  704. holder.appendChild(upgradeIcon);
  705. if (icon.transform) {
  706. upgradeIcon.style.transform = icon.transform;
  707. }
  708. })
  709. } else {
  710. let upgradeIcon = document.createElement("i");
  711. upgradeIcon.classList.add("fas");
  712. upgradeIcon.classList.add(value.icon);
  713. holder.appendChild(upgradeIcon);
  714. }
  715. button.addEventListener("mouseenter", function (e) { mouseTarget = button; upgradeTooltip(key, e); });
  716. button.addEventListener("mousemove", function (e) { mouseTarget = button; upgradeTooltip(key, e); });
  717. button.addEventListener("mouseleave", function () { mouseTarget = undefined; upgradeTooltipRemove(); });
  718. button.addEventListener("click", function (e) { buyUpgrade(key, e); });
  719. container.appendChild(button);
  720. }
  721. }
  722. function createDisplays() {
  723. const resourceList = document.querySelector("#resource-list");
  724. Object.keys(resourceTypes).forEach(key => {
  725. const quantity = document.createElement("div");
  726. quantity.classList.add("resource-quantity");
  727. quantity.classList.add("help-tooltip");
  728. quantity.dataset.tooltip = resourceTypes[key].desc;
  729. quantity.id = "resource-quantity-" + key;
  730. resourceList.appendChild(quantity);
  731. if (resourceTypes[key].generated) {
  732. const rate = document.createElement("div");
  733. rate.classList.add("resource-rate");
  734. rate.id = "resource-rate-" + key;
  735. resourceList.appendChild(rate);
  736. }
  737. })
  738. const statHolder = document.querySelector("#stats-holder");
  739. Object.keys(statTypes).forEach(key => {
  740. const div = document.createElement("div");
  741. div.classList.add("stat-line");
  742. const name = document.createElement("div");
  743. name.classList.add("stat-name");
  744. const value = document.createElement("div");
  745. value.classList.add("stat-value");
  746. value.id = "stat-value-" + key;
  747. name.innerText = statTypes[key].name;
  748. value.innerText = stats[key];
  749. div.appendChild(name);
  750. div.appendChild(value);
  751. statHolder.append(div);
  752. });
  753. const optionHolder = document.querySelector("#options-holder");
  754. Object.keys(options).forEach(key => {
  755. const div = document.createElement("div");
  756. div.classList.add("option-line");
  757. const name = document.createElement("div");
  758. name.classList.add("option-name");
  759. const value = document.createElement("input");
  760. value.classList.add("option-value");
  761. value.id = "option-value-" + key;
  762. name.innerText = options[key].name;
  763. value.innerText = options[key].get();
  764. div.appendChild(name);
  765. div.appendChild(value);
  766. optionHolder.append(div);
  767. });
  768. }
  769. function renderLine(line) {
  770. let div = document.createElement("div");
  771. div.innerText = line.text;
  772. if (line.valid !== undefined) {
  773. if (line.valid) {
  774. div.classList.add("cost-met");
  775. } else {
  776. div.classList.add("cost-unmet");
  777. }
  778. }
  779. if (line.class !== undefined) {
  780. for (let entry of line.class.split(",")) {
  781. div.classList.add(entry);
  782. }
  783. }
  784. return div;
  785. }
  786. function renderLines(lines) {
  787. let divs = [];
  788. for (let line of lines) {
  789. divs.push(renderLine(line));
  790. }
  791. return divs;
  792. }
  793. function renderCost(cost) {
  794. let list = [];
  795. list.push({
  796. "text": "Cost:"
  797. });
  798. for (const [key, value] of Object.entries(cost)) {
  799. list.push({
  800. "text": render(value, 0) + " " + resourceTypes[key].name,
  801. "valid": resources[key] >= value
  802. });
  803. }
  804. return renderLines(list);
  805. }
  806. function renderPrereqs(prereqs) {
  807. let list = [];
  808. if (prereqs === undefined) {
  809. return renderLines(list);
  810. }
  811. list.push({
  812. "text": "Own:"
  813. });
  814. for (const [key, value] of Object.entries(prereqs)) {
  815. if (key == "buildings") {
  816. for (const [id, amount] of Object.entries(prereqs.buildings)) {
  817. list.push({
  818. "text": buildings[id].name + " x" + render(amount, 0),
  819. "valid": belongings[id].count >= amount
  820. });
  821. }
  822. } else if (key == "productivity") {
  823. for (const [id, amount] of Object.entries(prereqs.productivity)) {
  824. list.push({
  825. "text": render(amount, 0) + " " + resourceTypes[id].name + "/s",
  826. "valid": currentProductivity[id] >= amount
  827. });
  828. }
  829. }
  830. }
  831. return renderLines(list);
  832. }
  833. function renderEffects(effectList) {
  834. let list = [];
  835. for (let effect of effectList) {
  836. list.push({ "text": effect_types[effect.type].desc(effect) });
  837. }
  838. return renderLines(list);
  839. }
  840. function clickPopup(text, type, location) {
  841. const div = document.createElement("div");
  842. div.textContent = text;
  843. div.classList.add("click-popup-" + type);
  844. var direction;
  845. if (type == "food") {
  846. direction = -150;
  847. } else if (type == "gulp") {
  848. direction = -150;
  849. } else if (type == "upgrade") {
  850. direction = -50;
  851. } else if (type == "info") {
  852. direction = 0;
  853. }
  854. direction *= Math.random() * 0.5 + 1;
  855. direction = Math.round(direction) + "px"
  856. div.style.setProperty("--target", direction)
  857. div.style.left = location[0] + "px";
  858. div.style.top = location[1] + "px";
  859. const body = document.querySelector("body");
  860. body.appendChild(div);
  861. setTimeout(() => {
  862. body.removeChild(div);
  863. }, 2000);
  864. }
  865. function doNews() {
  866. let options = [];
  867. let weights = [];
  868. let indices = [];
  869. let index = 0;
  870. news.forEach(entry => {
  871. if (entry.condition(state) && newsWeightFactors[index] != 1) {
  872. options = options.concat(entry.lines);
  873. weights.push(1 - newsWeightFactors[index])
  874. indices.push(index);
  875. }
  876. index += 1;
  877. });
  878. const choice = weightedSelect(weights);
  879. showNews(options[choice](state));
  880. for (let i = 0; i < newsWeightFactors.length; i++) {
  881. newsWeightFactors[i] *= 0.9;
  882. }
  883. newsWeightFactors[indices[choice]] = 1;
  884. newsShowTimer = setTimeout(() => {
  885. doNews();
  886. }, 8000);
  887. }
  888. function showNews(text) {
  889. const div = document.createElement("div");
  890. div.innerHTML = text;
  891. div.classList.add("news-text");
  892. const body = document.querySelector("body");
  893. div.addEventListener("click", () => {
  894. clearTimeout(newsShowTimer);
  895. clearTimeout(newsRemoveTimer);
  896. div.classList.add("news-text-leaving");
  897. setTimeout(() => {
  898. body.removeChild(div);
  899. }, 1000);
  900. doNews();
  901. });
  902. body.appendChild(div);
  903. newsRemoveTimer = setTimeout(() => {
  904. div.classList.add("news-text-leaving");
  905. setTimeout(() => {
  906. body.removeChild(div);
  907. }, 1000);
  908. }, 8000);
  909. }
  910. function doPowerup() {
  911. const lifetime = 10000;
  912. const button = document.createElement("div");
  913. const left = Math.round(Math.random() * 50 + 25) + "%";
  914. const top = Math.round(Math.random() * 50 + 25) + "%";
  915. button.classList.add("powerup");
  916. button.style.setProperty("--lifetime", lifetime / 1000 + "s");
  917. button.style.setProperty("--leftpos", left);
  918. button.style.setProperty("--toppos", top);
  919. const body = document.querySelector("body");
  920. body.appendChild(button);
  921. const choices = [];
  922. Object.entries(powerups).forEach(([key, val]) => {
  923. if (val.prereqs(state))
  924. choices.push(key);
  925. });
  926. const choice = Math.floor(Math.random() * choices.length);
  927. const powerup = powerups[choices[choice]];
  928. const icon = document.createElement("div");
  929. icon.classList.add("fas");
  930. icon.classList.add(powerup.icon);
  931. button.appendChild(icon);
  932. const remove = setTimeout(() => {
  933. body.removeChild(button);
  934. }, lifetime);
  935. let delay = 60000 + Math.random() * 30000;
  936. for (let effect of effects["powerup-freq"]) {
  937. if (ownedUpgrades[effect.parent]) {
  938. delay = effect.apply(delay);
  939. }
  940. }
  941. setTimeout(() => {
  942. doPowerup();
  943. }, delay);
  944. button.addEventListener("mousedown", e => {
  945. if (powerup.duration !== undefined) {
  946. addPowerup(choices[choice], powerup);
  947. } else {
  948. powerup.effect(state);
  949. }
  950. powerup.popup(powerup, e);
  951. button.classList.add("powerup-clicked");
  952. resources.powerups += 1;
  953. clearTimeout(remove);
  954. stats.powerups += 1;
  955. setTimeout(() => {
  956. body.removeChild(button);
  957. }, 500);
  958. });
  959. }
  960. function fillTooltip(type, field, content) {
  961. let item = document.querySelector("#" + type + "-tooltip-" + field);
  962. if (typeof (content) === "string") {
  963. item.innerText = content;
  964. } else {
  965. replaceChildren(item, content);
  966. }
  967. }
  968. function upgradeTooltip(id, event) {
  969. let tooltip = document.querySelector("#upgrade-tooltip");
  970. tooltip.style.setProperty("display", "inline-block");
  971. fillTooltip("upgrade", "name", upgrades[id].name);
  972. fillTooltip("upgrade", "desc", upgrades[id].desc);
  973. fillTooltip("upgrade", "effect", renderEffects(upgrades[id].effects));
  974. fillTooltip("upgrade", "cost", renderCost(upgrades[id].cost));
  975. fillTooltip("upgrade", "prereqs", renderPrereqs(upgrades[id].prereqs));
  976. let yOffset = tooltip.parentElement.getBoundingClientRect().y;
  977. let tooltipSize = tooltip.getBoundingClientRect().height;
  978. let yTrans = Math.round(event.clientY - yOffset);
  979. var body = document.body,
  980. html = document.documentElement;
  981. var height = Math.max(window.innerHeight);
  982. yTrans = Math.min(yTrans, height - tooltipSize - 150);
  983. tooltip.style.setProperty("transform", "translate(-420px, " + yTrans + "px)");
  984. }
  985. function upgradeTooltipRemove() {
  986. let tooltip = document.querySelector("#upgrade-tooltip");
  987. tooltip.style.setProperty("display", "none");
  988. }
  989. function prodSummary(id) {
  990. let list = [];
  991. list.push(
  992. { "text": "Each " + buildings[id].name + " produces " + render(belongings[id].count == 0 ? 0 : contributions[id].food / belongings[id].count, 3) + " food/sec" }
  993. );
  994. list.push(
  995. { "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" }
  996. );
  997. let percentage = round(100 * contributions[id].food / currentProductivity["food"], 2);
  998. if (isNaN(percentage)) {
  999. percentage = 0;
  1000. }
  1001. list.push(
  1002. { "text": "(" + percentage + "% of all food)" }
  1003. );
  1004. return renderLines(list);
  1005. }
  1006. function buildingTooltip(id, event) {
  1007. let tooltip = document.querySelector("#building-tooltip");
  1008. tooltip.style.setProperty("display", "inline-block");
  1009. const count = buildingCount();
  1010. fillTooltip("building", "name", (count != 1 ? count + "x " : "") + buildings[id].name);
  1011. fillTooltip("building", "desc", buildings[id].desc);
  1012. fillTooltip("building", "cost", render(costOfBuilding(id, count).food) + " food");
  1013. fillTooltip("building", "prod", prodSummary(id));
  1014. let xPos = tooltip.parentElement.getBoundingClientRect().x - 450;
  1015. // wow browsers are bad
  1016. var body = document.body,
  1017. html = document.documentElement;
  1018. var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
  1019. let yPos = Math.min(event.clientY, height - 200);
  1020. tooltip.style.setProperty("transform", "translate(" + xPos + "px, " + yPos + "px)")
  1021. }
  1022. function buildingTooltipRemove() {
  1023. let tooltip = document.querySelector("#building-tooltip");
  1024. tooltip.style.setProperty("display", "none");
  1025. }
  1026. function powerupTooltip(id, event) {
  1027. let tooltip = document.querySelector("#powerup-tooltip");
  1028. tooltip.style.setProperty("display", "inline-block");
  1029. fillTooltip("powerup", "name", powerups[id].name);
  1030. fillTooltip("powerup", "desc", powerups[id].description);
  1031. let xPos = tooltip.parentElement.getBoundingClientRect().x + 100;
  1032. // wow browsers are bad
  1033. var body = document.body,
  1034. html = document.documentElement;
  1035. var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
  1036. let yPos = Math.min(event.clientY - 100, height - 150);
  1037. tooltip.style.setProperty("transform", "translate(" + xPos + "px, " + yPos + "px)")
  1038. }
  1039. function powerupTooltipRemove() {
  1040. let tooltip = document.querySelector("#powerup-tooltip");
  1041. tooltip.style.setProperty("display", "none");
  1042. }
  1043. function helpTooltip(element, event) {
  1044. const text = element.dataset.tooltip;
  1045. let tooltip = document.querySelector("#help-tooltip");
  1046. tooltip.style.setProperty("display", "inline-block");
  1047. const count = buildingCount();
  1048. fillTooltip("help", "help", text);
  1049. let xPos = event.clientX + 100;
  1050. // wow browsers are bad
  1051. var body = document.body,
  1052. html = document.documentElement;
  1053. var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
  1054. let yPos = Math.min(event.clientY, height - 200);
  1055. tooltip.style.setProperty("transform", "translate(" + xPos + "px, " + yPos + "px)")
  1056. }
  1057. function helpTooltipRemove() {
  1058. let tooltip = document.querySelector("#help-tooltip");
  1059. tooltip.style.setProperty("display", "none");
  1060. }
  1061. window.onload = function () {
  1062. setup();
  1063. lastTime = performance.now();
  1064. doNews();
  1065. doPowerup();
  1066. setTimeout(updateDisplay, 1000 / updateRate);
  1067. setTimeout(autosave, 60000);
  1068. }
  1069. window.onblur = function() {
  1070. controlHeld = false;
  1071. shiftHeld = false;
  1072. }
  1073. window.onfocus = function() {
  1074. window.dispatchEvent(new Event("keydown"))
  1075. }
  1076. function autosave() {
  1077. saveGame();
  1078. let x = window.innerWidth / 2;
  1079. let y = window.innerHeight * 9 / 10;
  1080. clickPopup("Autosaving...", "info", [x, y]);
  1081. setTimeout(autosave, 60000);
  1082. }
  1083. function save(e) {
  1084. saveGame();
  1085. clickPopup("Saved!", "info", [e.clientX, e.clientY]);
  1086. }
  1087. function saveGame() {
  1088. try {
  1089. let storage = window.localStorage;
  1090. const save = {}
  1091. save.version = migrations.length;
  1092. save.ownedUpgrades = ownedUpgrades;
  1093. save.resources = resources;
  1094. save.belongings = belongings;
  1095. save.stats = stats;
  1096. save.macroDesc = macroDesc;
  1097. storage.setItem("save", JSON.stringify(save));
  1098. } catch (e) {
  1099. clickPopup("Can't save - no access to local storage.", "info", [window.innerWidth / 2, window.innerHeight / 5]);
  1100. }
  1101. }
  1102. const migrations = [
  1103. // dummy migration, because there was no version 0
  1104. save => {
  1105. },
  1106. // introduce stats
  1107. save => {
  1108. save.stats = {}
  1109. },
  1110. // introduce macroDesc
  1111. save => {
  1112. save.macroDesc = {}
  1113. }
  1114. ]
  1115. function migrate(save) {
  1116. let version = save.version;
  1117. while (version != migrations.length) {
  1118. migrations[version](save);
  1119. version += 1;
  1120. }
  1121. save.version = version;
  1122. }
  1123. function load() {
  1124. try {
  1125. let storage = window.localStorage;
  1126. // migrate to everything in one
  1127. if (storage.getItem("save-version") !== null) {
  1128. const save = {};
  1129. save.ownedUpgrades = JSON.parse(storage.getItem("ownedUpgrades"));
  1130. save.resources = JSON.parse(storage.getItem("resources"));
  1131. save.belongings = JSON.parse(storage.getItem("belongings"));
  1132. save.version = 1;
  1133. storage.clear();
  1134. storage.setItem("save", JSON.stringify(save))
  1135. }
  1136. const save = JSON.parse(storage.getItem("save"));
  1137. if (save == null)
  1138. return;
  1139. migrate(save);
  1140. for (const [key, value] of Object.entries(save.ownedUpgrades)) {
  1141. ownedUpgrades[key] = value;
  1142. }
  1143. for (const [key, value] of Object.entries(save.resources)) {
  1144. resources[key] = value;
  1145. }
  1146. for (const [key, value] of Object.entries(save.belongings)) {
  1147. belongings[key] = value;
  1148. }
  1149. for (const [key, value] of Object.entries(save.stats)) {
  1150. stats[key] = value;
  1151. }
  1152. for (const [key, value] of Object.entries(save.macroDesc)) {
  1153. macroDesc[key] = value;
  1154. }
  1155. } catch (e) {
  1156. console.error(e);
  1157. clickPopup("Can't load - no access to local storage.", "info", [window.innerWidth / 2, window.innerHeight / 5]);
  1158. }
  1159. }
  1160. function reset() {
  1161. window.localStorage.clear();
  1162. }
  1163. function cycleNumbers() {
  1164. numberMode = numberModes[numberMode.next];
  1165. updateOptions();
  1166. }