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.
 
 
 
 

1460 lines
35 KiB

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