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.
 
 
 
 

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