From 8f5525eb88364991e5c2f5dc36f16379d79ee06a Mon Sep 17 00:00:00 2001 From: EmeraldBlock Date: Fri, 20 May 2022 18:19:06 -0500 Subject: [PATCH] show exact speeds --- src/js/core/utils.js | 1559 +++++++++++----------- src/js/game/hud/parts/miner_highlight.js | 6 +- src/js/game/hud/parts/shop.js | 8 +- 3 files changed, 791 insertions(+), 782 deletions(-) diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 35e70310..319fe0ea 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -1,775 +1,784 @@ -import { T } from "../translations"; - -const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"]; - -/** - * Returns if this platform is android - * @returns {boolean} - */ -export function isAndroid() { - if (!G_IS_MOBILE_APP) { - return false; - } - const platform = window.device.platform; - return platform === "Android" || platform === "amazon-fireos"; -} - -/** - * Returns if this platform is iOs - * @returns {boolean} - */ -export function isIos() { - if (!G_IS_MOBILE_APP) { - return false; - } - return window.device.platform === "iOS"; -} - -/** - * Returns a platform name - * @returns {"android" | "browser" | "ios" | "standalone" | "unknown"} - */ -export function getPlatformName() { - if (G_IS_STANDALONE) { - return "standalone"; - } else if (G_IS_BROWSER) { - return "browser"; - } else if (G_IS_MOBILE_APP && isAndroid()) { - return "android"; - } else if (G_IS_MOBILE_APP && isIos()) { - return "ios"; - } - return "unknown"; -} - -/** - * Makes a new 2D array with undefined contents - * @param {number} w - * @param {number} h - * @returns {Array>} - */ -export function make2DUndefinedArray(w, h) { - const result = new Array(w); - for (let x = 0; x < w; ++x) { - result[x] = new Array(h); - } - return result; -} - -/** - * Creates a new map (an empty object without any props) - */ -export function newEmptyMap() { - return Object.create(null); -} - -/** - * Returns a random integer in the range [start,end] - * @param {number} start - * @param {number} end - */ -export function randomInt(start, end) { - return start + Math.round(Math.random() * (end - start)); -} - -/** - * Access an object in a very annoying way, used for obsfuscation. - * @param {any} obj - * @param {Array} keys - */ -export function accessNestedPropertyReverse(obj, keys) { - let result = obj; - for (let i = keys.length - 1; i >= 0; --i) { - result = result[keys[i]]; - } - return result; -} - -/** - * Chooses a random entry of an array - * @template T - * @param {T[]} arr - * @returns {T} - */ -export function randomChoice(arr) { - return arr[Math.floor(Math.random() * arr.length)]; -} - -/** - * Deletes from an array by swapping with the last element - * @param {Array} array - * @param {number} index - */ -export function fastArrayDelete(array, index) { - if (index < 0 || index >= array.length) { - throw new Error("Out of bounds"); - } - // When the element is not the last element - if (index !== array.length - 1) { - // Get the last element, and swap it with the one we want to delete - const last = array[array.length - 1]; - array[index] = last; - } - - // Finally remove the last element - array.length -= 1; -} - -/** - * Deletes from an array by swapping with the last element. Searches - * for the value in the array first - * @param {Array} array - * @param {any} value - */ -export function fastArrayDeleteValue(array, value) { - if (array == null) { - throw new Error("Tried to delete from non array!"); - } - const index = array.indexOf(value); - if (index < 0) { - console.error("Value", value, "not contained in array:", array, "!"); - return value; - } - return fastArrayDelete(array, index); -} - -/** - * @see fastArrayDeleteValue - * @param {Array} array - * @param {any} value - */ -export function fastArrayDeleteValueIfContained(array, value) { - if (array == null) { - throw new Error("Tried to delete from non array!"); - } - const index = array.indexOf(value); - if (index < 0) { - return value; - } - return fastArrayDelete(array, index); -} - -/** - * Deletes from an array at the given index - * @param {Array} array - * @param {number} index - */ -export function arrayDelete(array, index) { - if (index < 0 || index >= array.length) { - throw new Error("Out of bounds"); - } - array.splice(index, 1); -} - -/** - * Deletes the given value from an array - * @param {Array} array - * @param {any} value - */ -export function arrayDeleteValue(array, value) { - if (array == null) { - throw new Error("Tried to delete from non array!"); - } - const index = array.indexOf(value); - if (index < 0) { - console.error("Value", value, "not contained in array:", array, "!"); - return value; - } - return arrayDelete(array, index); -} - -/** - * Compare two floats for epsilon equality - * @param {number} a - * @param {number} b - * @returns {boolean} - */ -export function epsilonCompare(a, b, epsilon = 1e-5) { - return Math.abs(a - b) < epsilon; -} - -/** - * Interpolates two numbers - * @param {number} a - * @param {number} b - * @param {number} x Mix factor, 0 means 100% a, 1 means 100%b, rest is interpolated - */ -export function lerp(a, b, x) { - return a * (1 - x) + b * x; -} - -/** - * Finds a value which is nice to display, e.g. 15669 -> 15000. Also handles fractional stuff - * @param {number} num - */ -export function findNiceValue(num) { - if (num > 1e8) { - return num; - } - if (num < 0.00001) { - return 0; - } - - let roundAmount = 1; - if (num > 50000) { - roundAmount = 10000; - } else if (num > 20000) { - roundAmount = 5000; - } else if (num > 5000) { - roundAmount = 1000; - } else if (num > 2000) { - roundAmount = 500; - } else if (num > 1000) { - roundAmount = 100; - } else if (num > 100) { - roundAmount = 20; - } else if (num > 20) { - roundAmount = 5; - } - - const niceValue = Math.floor(num / roundAmount) * roundAmount; - if (num >= 10) { - return Math.round(niceValue); - } - if (num >= 1) { - return Math.round(niceValue * 10) / 10; - } - - return Math.round(niceValue * 100) / 100; -} - -/** - * Finds a nice integer value - * @see findNiceValue - * @param {number} num - */ -export function findNiceIntegerValue(num) { - return Math.ceil(findNiceValue(num)); -} - -/** - * Formats a big number - * @param {number} num - * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') - * @returns {string} - */ -export function formatBigNumber(num, separator = T.global.decimalSeparator) { - const sign = num < 0 ? "-" : ""; - num = Math.abs(num); - - if (num > 1e54) { - return sign + T.global.infinite; - } - - if (num < 10 && !Number.isInteger(num)) { - return sign + num.toFixed(2); - } - if (num < 50 && !Number.isInteger(num)) { - return sign + num.toFixed(1); - } - num = Math.floor(num); - - if (num < 1000) { - return sign + "" + num; - } else { - let leadingDigits = num; - let suffix = ""; - for (let suffixIndex = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) { - leadingDigits = leadingDigits / 1000; - suffix = T.global.suffix[bigNumberSuffixTranslationKeys[suffixIndex]]; - if (leadingDigits < 1000) { - break; - } - } - const leadingDigitsRounded = round1Digit(leadingDigits); - const leadingDigitsNoTrailingDecimal = leadingDigitsRounded - .toString() - .replace(".0", "") - .replace(".", separator); - return sign + leadingDigitsNoTrailingDecimal + suffix; - } -} - -/** - * Formats a big number, but does not add any suffix and instead uses its full representation - * @param {number} num - * @param {string=} divider The divider for numbers like 50,000 (divider=',') - * @returns {string} - */ -export function formatBigNumberFull(num, divider = T.global.thousandsDivider) { - if (num < 1000) { - return num + ""; - } - if (num > 1e54) { - return T.global.infinite; - } - let rest = num; - let out = ""; - while (rest >= 1000) { - out = (rest % 1000).toString().padStart(3, "0") + divider + out; - rest = Math.floor(rest / 1000); - } - out = rest + divider + out; - - return out.substring(0, out.length - 1); -} - -/** - * Waits two frames so the ui is updated - * @returns {Promise} - */ -export function waitNextFrame() { - return new Promise(function (resolve) { - window.requestAnimationFrame(function () { - window.requestAnimationFrame(function () { - resolve(); - }); - }); - }); -} - -/** - * Rounds 1 digit - * @param {number} n - * @returns {number} - */ -export function round1Digit(n) { - return Math.floor(n * 10.0) / 10.0; -} - -/** - * Rounds 2 digits - * @param {number} n - * @returns {number} - */ -export function round2Digits(n) { - return Math.floor(n * 100.0) / 100.0; -} - -/** - * Rounds 3 digits - * @param {number} n - * @returns {number} - */ -export function round3Digits(n) { - return Math.floor(n * 1000.0) / 1000.0; -} - -/** - * Rounds 4 digits - * @param {number} n - * @returns {number} - */ -export function round4Digits(n) { - return Math.floor(n * 10000.0) / 10000.0; -} - -/** - * Clamps a value between [min, max] - * @param {number} v - * @param {number=} minimum Default 0 - * @param {number=} maximum Default 1 - */ -export function clamp(v, minimum = 0, maximum = 1) { - return Math.max(minimum, Math.min(maximum, v)); -} - -/** - * Helper method to create a new div element - * @param {string=} id - * @param {Array=} classes - * @param {string=} innerHTML - */ -export function makeDivElement(id = null, classes = [], innerHTML = "") { - const div = document.createElement("div"); - if (id) { - div.id = id; - } - for (let i = 0; i < classes.length; ++i) { - div.classList.add(classes[i]); - } - div.innerHTML = innerHTML; - return div; -} - -/** - * Helper method to create a new div - * @param {Element} parent - * @param {string=} id - * @param {Array=} classes - * @param {string=} innerHTML - */ -export function makeDiv(parent, id = null, classes = [], innerHTML = "") { - const div = makeDivElement(id, classes, innerHTML); - parent.appendChild(div); - return div; -} - -/** - * Helper method to create a new button element - * @param {Array=} classes - * @param {string=} innerHTML - */ -export function makeButtonElement(classes = [], innerHTML = "") { - const element = document.createElement("button"); - for (let i = 0; i < classes.length; ++i) { - element.classList.add(classes[i]); - } - element.classList.add("styledButton"); - element.innerHTML = innerHTML; - return element; -} - -/** - * Helper method to create a new button - * @param {Element} parent - * @param {Array=} classes - * @param {string=} innerHTML - */ -export function makeButton(parent, classes = [], innerHTML = "") { - const element = makeButtonElement(classes, innerHTML); - parent.appendChild(element); - return element; -} - -/** - * Removes all children of the given element - * @param {Element} elem - */ -export function removeAllChildren(elem) { - if (elem) { - var range = document.createRange(); - range.selectNodeContents(elem); - range.deleteContents(); - } -} - -/** - * Returns if the game supports this browser - */ -export function isSupportedBrowser() { - // please note, - // that IE11 now returns undefined again for window.chrome - // and new Opera 30 outputs true for window.chrome - // but needs to check if window.opr is not undefined - // and new IE Edge outputs to true now for window.chrome - // and if not iOS Chrome check - // so use the below updated condition - - if (G_IS_MOBILE_APP || G_IS_STANDALONE) { - return true; - } - - // @ts-ignore - var isChromium = window.chrome; - var winNav = window.navigator; - var vendorName = winNav.vendor; - // @ts-ignore - var isIEedge = winNav.userAgent.indexOf("Edge") > -1; - var isIOSChrome = winNav.userAgent.match("CriOS"); - - if (isIOSChrome) { - // is Google Chrome on IOS - return false; - } else if ( - isChromium !== null && - typeof isChromium !== "undefined" && - vendorName === "Google Inc." && - isIEedge === false - ) { - // is Google Chrome - return true; - } else { - // not Google Chrome - return false; - } -} - -/** - * Formats an amount of seconds into something like "5s ago" - * @param {number} secs Seconds - * @returns {string} - */ -export function formatSecondsToTimeAgo(secs) { - const seconds = Math.floor(secs); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (seconds < 60) { - if (seconds === 1) { - return T.global.time.oneSecondAgo; - } - return T.global.time.xSecondsAgo.replace("", "" + seconds); - } else if (minutes < 60) { - if (minutes === 1) { - return T.global.time.oneMinuteAgo; - } - return T.global.time.xMinutesAgo.replace("", "" + minutes); - } else if (hours < 24) { - if (hours === 1) { - return T.global.time.oneHourAgo; - } - return T.global.time.xHoursAgo.replace("", "" + hours); - } else { - if (days === 1) { - return T.global.time.oneDayAgo; - } - return T.global.time.xDaysAgo.replace("", "" + days); - } -} - -/** - * Formats seconds into a readable string like "5h 23m" - * @param {number} secs Seconds - * @returns {string} - */ -export function formatSeconds(secs) { - const trans = T.global.time; - secs = Math.ceil(secs); - if (secs < 60) { - return trans.secondsShort.replace("", "" + secs); - } else if (secs < 60 * 60) { - const minutes = Math.floor(secs / 60); - const seconds = secs % 60; - return trans.minutesAndSecondsShort - .replace("", "" + seconds) - .replace("", "" + minutes); - } else { - const hours = Math.floor(secs / 3600); - const minutes = Math.floor(secs / 60) % 60; - return trans.hoursAndMinutesShort.replace("", "" + minutes).replace("", "" + hours); - } -} - -/** - * Formats a number like 2.51 to "2.5" - * @param {number} speed - * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') - */ -export function round1DigitLocalized(speed, separator = T.global.decimalSeparator) { - return round1Digit(speed).toString().replace(".", separator); -} - -/** - * Formats a number like 2.51 to "2.51 items / s" - * @param {number} speed - * @param {boolean=} double - * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') - */ -export function formatItemsPerSecond(speed, double = false, separator = T.global.decimalSeparator) { - return ( - (speed === 1.0 - ? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond - : T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace( - "", - round2Digits(speed).toString().replace(".", separator) - )) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : "") - ); -} - -/** - * Rotates a flat 3x3 matrix clockwise - * Entries: - * 0 lo - * 1 mo - * 2 ro - * 3 lm - * 4 mm - * 5 rm - * 6 lu - * 7 mu - * 8 ru - * @param {Array} flatMatrix - */ - -export function rotateFlatMatrix3x3(flatMatrix) { - return [ - flatMatrix[6], - flatMatrix[3], - flatMatrix[0], - flatMatrix[7], - flatMatrix[4], - flatMatrix[1], - flatMatrix[8], - flatMatrix[5], - flatMatrix[2], - ]; -} - -/** - * Generates rotated variants of the matrix - * @param {Array} originalMatrix - * @returns {Object>} - */ -export function generateMatrixRotations(originalMatrix) { - const result = { - 0: originalMatrix, - }; - - originalMatrix = rotateFlatMatrix3x3(originalMatrix); - result[90] = originalMatrix; - - originalMatrix = rotateFlatMatrix3x3(originalMatrix); - result[180] = originalMatrix; - - originalMatrix = rotateFlatMatrix3x3(originalMatrix); - result[270] = originalMatrix; - - return result; -} - -/** - * - * @typedef {{ - * top: any, - * right: any, - * bottom: any, - * left: any - * }} DirectionalObject - */ - -/** - * Rotates a directional object - * @param {DirectionalObject} obj - * @returns {DirectionalObject} - */ -export function rotateDirectionalObject(obj, rotation) { - const queue = [obj.top, obj.right, obj.bottom, obj.left]; - while (rotation !== 0) { - rotation -= 90; - queue.push(queue.shift()); - } - - return { - top: queue[0], - right: queue[1], - bottom: queue[2], - left: queue[3], - }; -} - -/** - * Modulo which works for negative numbers - * @param {number} n - * @param {number} m - */ -export function safeModulo(n, m) { - return ((n % m) + m) % m; -} - -/** - * Returns a smooth pulse between 0 and 1 - * @param {number} time time in seconds - * @returns {number} - */ -export function smoothPulse(time) { - return Math.sin(time * 4) * 0.5 + 0.5; -} - -/** - * Fills in a tag - * @param {string} translation - * @param {string} link - */ -export function fillInLinkIntoTranslation(translation, link) { - return translation - .replace("", "") - .replace("", ""); -} - -/** - * Generates a file download - * @param {string} filename - * @param {string} text - */ -export function generateFileDownload(filename, text) { - var element = document.createElement("a"); - element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); - element.setAttribute("download", filename); - - element.style.display = "none"; - document.body.appendChild(element); - - element.click(); - document.body.removeChild(element); -} - -/** - * Starts a file chooser - * @param {string} acceptedType - */ -export function startFileChoose(acceptedType = ".bin") { - var input = document.createElement("input"); - input.type = "file"; - input.accept = acceptedType; - - return new Promise(resolve => { - input.onchange = _ => resolve(input.files[0]); - input.click(); - }); -} - -const MAX_ROMAN_NUMBER = 49; -const romanLiteralsCache = ["0"]; - -/** - * - * @param {number} number - * @returns {string} - */ -export function getRomanNumber(number) { - if (G_WEGAME_VERSION) { - return String(number); - } - - number = Math.max(0, Math.round(number)); - if (romanLiteralsCache[number]) { - return romanLiteralsCache[number]; - } - - if (number > MAX_ROMAN_NUMBER) { - return String(number); - } - - function formatDigit(digit, unit, quintuple, decuple) { - switch (digit) { - case 0: - return ""; - case 1: // I - return unit; - case 2: // II - return unit + unit; - case 3: // III - return unit + unit + unit; - case 4: // IV - return unit + quintuple; - case 9: // IX - return unit + decuple; - default: - // V, VI, VII, VIII - return quintuple + formatDigit(digit - 5, unit, quintuple, decuple); - } - } - - let thousands = Math.floor(number / 1000); - let thousandsPart = ""; - while (thousands > 0) { - thousandsPart += "M"; - thousands -= 1; - } - - const hundreds = Math.floor((number % 1000) / 100); - const hundredsPart = formatDigit(hundreds, "C", "D", "M"); - - const tens = Math.floor((number % 100) / 10); - const tensPart = formatDigit(tens, "X", "L", "C"); - - const units = number % 10; - const unitsPart = formatDigit(units, "I", "V", "X"); - - const formatted = thousandsPart + hundredsPart + tensPart + unitsPart; - - romanLiteralsCache[number] = formatted; - return formatted; -} +import { T } from "../translations"; + +const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"]; + +/** + * Returns if this platform is android + * @returns {boolean} + */ +export function isAndroid() { + if (!G_IS_MOBILE_APP) { + return false; + } + const platform = window.device.platform; + return platform === "Android" || platform === "amazon-fireos"; +} + +/** + * Returns if this platform is iOs + * @returns {boolean} + */ +export function isIos() { + if (!G_IS_MOBILE_APP) { + return false; + } + return window.device.platform === "iOS"; +} + +/** + * Returns a platform name + * @returns {"android" | "browser" | "ios" | "standalone" | "unknown"} + */ +export function getPlatformName() { + if (G_IS_STANDALONE) { + return "standalone"; + } else if (G_IS_BROWSER) { + return "browser"; + } else if (G_IS_MOBILE_APP && isAndroid()) { + return "android"; + } else if (G_IS_MOBILE_APP && isIos()) { + return "ios"; + } + return "unknown"; +} + +/** + * Makes a new 2D array with undefined contents + * @param {number} w + * @param {number} h + * @returns {Array>} + */ +export function make2DUndefinedArray(w, h) { + const result = new Array(w); + for (let x = 0; x < w; ++x) { + result[x] = new Array(h); + } + return result; +} + +/** + * Creates a new map (an empty object without any props) + */ +export function newEmptyMap() { + return Object.create(null); +} + +/** + * Returns a random integer in the range [start,end] + * @param {number} start + * @param {number} end + */ +export function randomInt(start, end) { + return start + Math.round(Math.random() * (end - start)); +} + +/** + * Access an object in a very annoying way, used for obsfuscation. + * @param {any} obj + * @param {Array} keys + */ +export function accessNestedPropertyReverse(obj, keys) { + let result = obj; + for (let i = keys.length - 1; i >= 0; --i) { + result = result[keys[i]]; + } + return result; +} + +/** + * Chooses a random entry of an array + * @template T + * @param {T[]} arr + * @returns {T} + */ +export function randomChoice(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +/** + * Deletes from an array by swapping with the last element + * @param {Array} array + * @param {number} index + */ +export function fastArrayDelete(array, index) { + if (index < 0 || index >= array.length) { + throw new Error("Out of bounds"); + } + // When the element is not the last element + if (index !== array.length - 1) { + // Get the last element, and swap it with the one we want to delete + const last = array[array.length - 1]; + array[index] = last; + } + + // Finally remove the last element + array.length -= 1; +} + +/** + * Deletes from an array by swapping with the last element. Searches + * for the value in the array first + * @param {Array} array + * @param {any} value + */ +export function fastArrayDeleteValue(array, value) { + if (array == null) { + throw new Error("Tried to delete from non array!"); + } + const index = array.indexOf(value); + if (index < 0) { + console.error("Value", value, "not contained in array:", array, "!"); + return value; + } + return fastArrayDelete(array, index); +} + +/** + * @see fastArrayDeleteValue + * @param {Array} array + * @param {any} value + */ +export function fastArrayDeleteValueIfContained(array, value) { + if (array == null) { + throw new Error("Tried to delete from non array!"); + } + const index = array.indexOf(value); + if (index < 0) { + return value; + } + return fastArrayDelete(array, index); +} + +/** + * Deletes from an array at the given index + * @param {Array} array + * @param {number} index + */ +export function arrayDelete(array, index) { + if (index < 0 || index >= array.length) { + throw new Error("Out of bounds"); + } + array.splice(index, 1); +} + +/** + * Deletes the given value from an array + * @param {Array} array + * @param {any} value + */ +export function arrayDeleteValue(array, value) { + if (array == null) { + throw new Error("Tried to delete from non array!"); + } + const index = array.indexOf(value); + if (index < 0) { + console.error("Value", value, "not contained in array:", array, "!"); + return value; + } + return arrayDelete(array, index); +} + +/** + * Compare two floats for epsilon equality + * @param {number} a + * @param {number} b + * @returns {boolean} + */ +export function epsilonCompare(a, b, epsilon = 1e-5) { + return Math.abs(a - b) < epsilon; +} + +/** + * Interpolates two numbers + * @param {number} a + * @param {number} b + * @param {number} x Mix factor, 0 means 100% a, 1 means 100%b, rest is interpolated + */ +export function lerp(a, b, x) { + return a * (1 - x) + b * x; +} + +/** + * Finds a value which is nice to display, e.g. 15669 -> 15000. Also handles fractional stuff + * @param {number} num + */ +export function findNiceValue(num) { + if (num > 1e8) { + return num; + } + if (num < 0.00001) { + return 0; + } + + let roundAmount = 1; + if (num > 50000) { + roundAmount = 10000; + } else if (num > 20000) { + roundAmount = 5000; + } else if (num > 5000) { + roundAmount = 1000; + } else if (num > 2000) { + roundAmount = 500; + } else if (num > 1000) { + roundAmount = 100; + } else if (num > 100) { + roundAmount = 20; + } else if (num > 20) { + roundAmount = 5; + } + + const niceValue = Math.floor(num / roundAmount) * roundAmount; + if (num >= 10) { + return Math.round(niceValue); + } + if (num >= 1) { + return Math.round(niceValue * 10) / 10; + } + + return Math.round(niceValue * 100) / 100; +} + +/** + * Finds a nice integer value + * @see findNiceValue + * @param {number} num + */ +export function findNiceIntegerValue(num) { + return Math.ceil(findNiceValue(num)); +} + +/** + * Formats a big number + * @param {number} num + * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') + * @returns {string} + */ +export function formatBigNumber(num, separator = T.global.decimalSeparator) { + const sign = num < 0 ? "-" : ""; + num = Math.abs(num); + + if (num > 1e54) { + return sign + T.global.infinite; + } + + if (num < 10 && !Number.isInteger(num)) { + return sign + num.toFixed(2); + } + if (num < 50 && !Number.isInteger(num)) { + return sign + num.toFixed(1); + } + num = Math.floor(num); + + if (num < 1000) { + return sign + "" + num; + } else { + let leadingDigits = num; + let suffix = ""; + for (let suffixIndex = 0; suffixIndex < bigNumberSuffixTranslationKeys.length; ++suffixIndex) { + leadingDigits = leadingDigits / 1000; + suffix = T.global.suffix[bigNumberSuffixTranslationKeys[suffixIndex]]; + if (leadingDigits < 1000) { + break; + } + } + const leadingDigitsRounded = round1Digit(leadingDigits); + const leadingDigitsNoTrailingDecimal = leadingDigitsRounded + .toString() + .replace(".0", "") + .replace(".", separator); + return sign + leadingDigitsNoTrailingDecimal + suffix; + } +} + +/** + * Formats a big number, but does not add any suffix and instead uses its full representation + * @param {number} num + * @param {string=} divider The divider for numbers like 50,000 (divider=',') + * @returns {string} + */ +export function formatBigNumberFull(num, divider = T.global.thousandsDivider) { + if (num < 1000) { + return num + ""; + } + if (num > 1e54) { + return T.global.infinite; + } + let rest = num; + let out = ""; + while (rest >= 1000) { + out = (rest % 1000).toString().padStart(3, "0") + divider + out; + rest = Math.floor(rest / 1000); + } + out = rest + divider + out; + + return out.substring(0, out.length - 1); +} + +/** + * Waits two frames so the ui is updated + * @returns {Promise} + */ +export function waitNextFrame() { + return new Promise(function (resolve) { + window.requestAnimationFrame(function () { + window.requestAnimationFrame(function () { + resolve(); + }); + }); + }); +} + +/** + * Rounds 1 digit + * @param {number} n + * @returns {number} + */ +export function round1Digit(n) { + return Math.floor(n * 10.0) / 10.0; +} + +/** + * Rounds 2 digits + * @param {number} n + * @returns {number} + */ +export function round2Digits(n) { + return Math.floor(n * 100.0) / 100.0; +} + +/** + * Rounds 3 digits + * @param {number} n + * @returns {number} + */ +export function round3Digits(n) { + return Math.floor(n * 1000.0) / 1000.0; +} + +/** + * Rounds 4 digits + * @param {number} n + * @returns {number} + */ +export function round4Digits(n) { + return Math.floor(n * 10000.0) / 10000.0; +} + +/** + * Clamps a value between [min, max] + * @param {number} v + * @param {number=} minimum Default 0 + * @param {number=} maximum Default 1 + */ +export function clamp(v, minimum = 0, maximum = 1) { + return Math.max(minimum, Math.min(maximum, v)); +} + +/** + * Helper method to create a new div element + * @param {string=} id + * @param {Array=} classes + * @param {string=} innerHTML + */ +export function makeDivElement(id = null, classes = [], innerHTML = "") { + const div = document.createElement("div"); + if (id) { + div.id = id; + } + for (let i = 0; i < classes.length; ++i) { + div.classList.add(classes[i]); + } + div.innerHTML = innerHTML; + return div; +} + +/** + * Helper method to create a new div + * @param {Element} parent + * @param {string=} id + * @param {Array=} classes + * @param {string=} innerHTML + */ +export function makeDiv(parent, id = null, classes = [], innerHTML = "") { + const div = makeDivElement(id, classes, innerHTML); + parent.appendChild(div); + return div; +} + +/** + * Helper method to create a new button element + * @param {Array=} classes + * @param {string=} innerHTML + */ +export function makeButtonElement(classes = [], innerHTML = "") { + const element = document.createElement("button"); + for (let i = 0; i < classes.length; ++i) { + element.classList.add(classes[i]); + } + element.classList.add("styledButton"); + element.innerHTML = innerHTML; + return element; +} + +/** + * Helper method to create a new button + * @param {Element} parent + * @param {Array=} classes + * @param {string=} innerHTML + */ +export function makeButton(parent, classes = [], innerHTML = "") { + const element = makeButtonElement(classes, innerHTML); + parent.appendChild(element); + return element; +} + +/** + * Removes all children of the given element + * @param {Element} elem + */ +export function removeAllChildren(elem) { + if (elem) { + var range = document.createRange(); + range.selectNodeContents(elem); + range.deleteContents(); + } +} + +/** + * Returns if the game supports this browser + */ +export function isSupportedBrowser() { + // please note, + // that IE11 now returns undefined again for window.chrome + // and new Opera 30 outputs true for window.chrome + // but needs to check if window.opr is not undefined + // and new IE Edge outputs to true now for window.chrome + // and if not iOS Chrome check + // so use the below updated condition + + if (G_IS_MOBILE_APP || G_IS_STANDALONE) { + return true; + } + + // @ts-ignore + var isChromium = window.chrome; + var winNav = window.navigator; + var vendorName = winNav.vendor; + // @ts-ignore + var isIEedge = winNav.userAgent.indexOf("Edge") > -1; + var isIOSChrome = winNav.userAgent.match("CriOS"); + + if (isIOSChrome) { + // is Google Chrome on IOS + return false; + } else if ( + isChromium !== null && + typeof isChromium !== "undefined" && + vendorName === "Google Inc." && + isIEedge === false + ) { + // is Google Chrome + return true; + } else { + // not Google Chrome + return false; + } +} + +/** + * Formats an amount of seconds into something like "5s ago" + * @param {number} secs Seconds + * @returns {string} + */ +export function formatSecondsToTimeAgo(secs) { + const seconds = Math.floor(secs); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (seconds < 60) { + if (seconds === 1) { + return T.global.time.oneSecondAgo; + } + return T.global.time.xSecondsAgo.replace("", "" + seconds); + } else if (minutes < 60) { + if (minutes === 1) { + return T.global.time.oneMinuteAgo; + } + return T.global.time.xMinutesAgo.replace("", "" + minutes); + } else if (hours < 24) { + if (hours === 1) { + return T.global.time.oneHourAgo; + } + return T.global.time.xHoursAgo.replace("", "" + hours); + } else { + if (days === 1) { + return T.global.time.oneDayAgo; + } + return T.global.time.xDaysAgo.replace("", "" + days); + } +} + +/** + * Formats seconds into a readable string like "5h 23m" + * @param {number} secs Seconds + * @returns {string} + */ +export function formatSeconds(secs) { + const trans = T.global.time; + secs = Math.ceil(secs); + if (secs < 60) { + return trans.secondsShort.replace("", "" + secs); + } else if (secs < 60 * 60) { + const minutes = Math.floor(secs / 60); + const seconds = secs % 60; + return trans.minutesAndSecondsShort + .replace("", "" + seconds) + .replace("", "" + minutes); + } else { + const hours = Math.floor(secs / 3600); + const minutes = Math.floor(secs / 60) % 60; + return trans.hoursAndMinutesShort.replace("", "" + minutes).replace("", "" + hours); + } +} + +/** + * Formats a number like 2.51 to "2.51" + * @param {number} speed + * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') + */ +export function formatNumber(speed, separator = T.global.decimalSeparator) { + return speed.toString().replace(".", separator); +} + +/** + * Formats a number like 2.51 to "2.5" + * @param {number} speed + * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') + */ +export function round1DigitLocalized(speed, separator = T.global.decimalSeparator) { + return round1Digit(speed).toString().replace(".", separator); +} + +/** + * Formats a number like 2.51 to "2.51 items / s" + * @param {number} speed + * @param {boolean=} double + * @param {string=} separator The decimal separator for numbers like 50.1 (separator='.') + */ +export function formatItemsPerSecond(speed, double = false, separator = T.global.decimalSeparator) { + return ( + (speed === 1.0 + ? T.ingame.buildingPlacement.infoTexts.oneItemPerSecond + : T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace( + "", + speed.toString().replace(".", separator) + )) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : "") + ); +} + +/** + * Rotates a flat 3x3 matrix clockwise + * Entries: + * 0 lo + * 1 mo + * 2 ro + * 3 lm + * 4 mm + * 5 rm + * 6 lu + * 7 mu + * 8 ru + * @param {Array} flatMatrix + */ + +export function rotateFlatMatrix3x3(flatMatrix) { + return [ + flatMatrix[6], + flatMatrix[3], + flatMatrix[0], + flatMatrix[7], + flatMatrix[4], + flatMatrix[1], + flatMatrix[8], + flatMatrix[5], + flatMatrix[2], + ]; +} + +/** + * Generates rotated variants of the matrix + * @param {Array} originalMatrix + * @returns {Object>} + */ +export function generateMatrixRotations(originalMatrix) { + const result = { + 0: originalMatrix, + }; + + originalMatrix = rotateFlatMatrix3x3(originalMatrix); + result[90] = originalMatrix; + + originalMatrix = rotateFlatMatrix3x3(originalMatrix); + result[180] = originalMatrix; + + originalMatrix = rotateFlatMatrix3x3(originalMatrix); + result[270] = originalMatrix; + + return result; +} + +/** + * + * @typedef {{ + * top: any, + * right: any, + * bottom: any, + * left: any + * }} DirectionalObject + */ + +/** + * Rotates a directional object + * @param {DirectionalObject} obj + * @returns {DirectionalObject} + */ +export function rotateDirectionalObject(obj, rotation) { + const queue = [obj.top, obj.right, obj.bottom, obj.left]; + while (rotation !== 0) { + rotation -= 90; + queue.push(queue.shift()); + } + + return { + top: queue[0], + right: queue[1], + bottom: queue[2], + left: queue[3], + }; +} + +/** + * Modulo which works for negative numbers + * @param {number} n + * @param {number} m + */ +export function safeModulo(n, m) { + return ((n % m) + m) % m; +} + +/** + * Returns a smooth pulse between 0 and 1 + * @param {number} time time in seconds + * @returns {number} + */ +export function smoothPulse(time) { + return Math.sin(time * 4) * 0.5 + 0.5; +} + +/** + * Fills in a tag + * @param {string} translation + * @param {string} link + */ +export function fillInLinkIntoTranslation(translation, link) { + return translation + .replace("", "") + .replace("", ""); +} + +/** + * Generates a file download + * @param {string} filename + * @param {string} text + */ +export function generateFileDownload(filename, text) { + var element = document.createElement("a"); + element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); + element.setAttribute("download", filename); + + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); +} + +/** + * Starts a file chooser + * @param {string} acceptedType + */ +export function startFileChoose(acceptedType = ".bin") { + var input = document.createElement("input"); + input.type = "file"; + input.accept = acceptedType; + + return new Promise(resolve => { + input.onchange = _ => resolve(input.files[0]); + input.click(); + }); +} + +const MAX_ROMAN_NUMBER = 49; +const romanLiteralsCache = ["0"]; + +/** + * + * @param {number} number + * @returns {string} + */ +export function getRomanNumber(number) { + if (G_WEGAME_VERSION) { + return String(number); + } + + number = Math.max(0, Math.round(number)); + if (romanLiteralsCache[number]) { + return romanLiteralsCache[number]; + } + + if (number > MAX_ROMAN_NUMBER) { + return String(number); + } + + function formatDigit(digit, unit, quintuple, decuple) { + switch (digit) { + case 0: + return ""; + case 1: // I + return unit; + case 2: // II + return unit + unit; + case 3: // III + return unit + unit + unit; + case 4: // IV + return unit + quintuple; + case 9: // IX + return unit + decuple; + default: + // V, VI, VII, VIII + return quintuple + formatDigit(digit - 5, unit, quintuple, decuple); + } + } + + let thousands = Math.floor(number / 1000); + let thousandsPart = ""; + while (thousands > 0) { + thousandsPart += "M"; + thousands -= 1; + } + + const hundreds = Math.floor((number % 1000) / 100); + const hundredsPart = formatDigit(hundreds, "C", "D", "M"); + + const tens = Math.floor((number % 100) / 10); + const tensPart = formatDigit(tens, "X", "L", "C"); + + const units = number % 10; + const unitsPart = formatDigit(units, "I", "V", "X"); + + const formatted = thousandsPart + hundredsPart + tensPart + unitsPart; + + romanLiteralsCache[number] = formatted; + return formatted; +} diff --git a/src/js/game/hud/parts/miner_highlight.js b/src/js/game/hud/parts/miner_highlight.js index a0c6919d..435c92dd 100644 --- a/src/js/game/hud/parts/miner_highlight.js +++ b/src/js/game/hud/parts/miner_highlight.js @@ -1,5 +1,5 @@ import { globalConfig } from "../../../core/config"; -import { formatItemsPerSecond, round2Digits } from "../../../core/utils"; +import { formatItemsPerSecond, round4Digits } from "../../../core/utils"; import { Vector } from "../../../core/vector"; import { T } from "../../../translations"; import { Entity } from "../../entity"; @@ -69,7 +69,7 @@ export class HUDMinerHighlight extends BaseHUDPart { parameters.context.fill(); } - const throughput = round2Digits(connectedEntities.length * this.root.hubGoals.getMinerBaseSpeed()); + const throughput = round4Digits(connectedEntities.length * this.root.hubGoals.getMinerBaseSpeed()); const maxThroughput = this.root.hubGoals.getBeltBaseSpeed(); @@ -84,7 +84,7 @@ export class HUDMinerHighlight extends BaseHUDPart { parameters.context.beginRoundedRect( tooltipLocation.x + 5 * scale, tooltipLocation.y - 3 * scale, - (isCapped ? 100 : 65) * scale, + (isCapped ? 100 : 75) * scale, (isCapped ? 45 : 30) * scale, 2 ); diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index 0ceb770e..b5c0b1aa 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -1,6 +1,6 @@ import { ClickDetector } from "../../../core/click_detector"; import { InputReceiver } from "../../../core/input_receiver"; -import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils"; +import { formatBigNumber, formatNumber, getRomanNumber, makeDiv } from "../../../core/utils"; import { T } from "../../../translations"; import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { BaseHUDPart } from "../base_hud_part"; @@ -91,15 +91,15 @@ export class HUDShop extends BaseHUDPart { // Max level handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace( "", - formatBigNumber(currentTierMultiplier) + formatNumber(currentTierMultiplier) ); continue; } // Set description handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description - .replace("", formatBigNumber(currentTierMultiplier)) - .replace("", formatBigNumber(currentTierMultiplier + tierHandle.improvement / 10)); + .replace("", formatNumber(currentTierMultiplier)) + .replace("", formatNumber(currentTierMultiplier + tierHandle.improvement / 10)); tierHandle.required.forEach(({ shape, amount }) => { const container = makeDiv(handle.elemRequirements, null, ["requirement"]);