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.
tobspr_shapez.io/src/js/core/utils.js

776 lines
20 KiB

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<Array<any>>}
*/
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<string>} 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<any>} 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<any>} 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<any>} 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<any>} 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<any>} 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<void>}
*/
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<string>=} 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<string>=} 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<string>=} 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<string>=} 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("<x>", "" + seconds);
} else if (minutes < 60) {
if (minutes === 1) {
return T.global.time.oneMinuteAgo;
}
return T.global.time.xMinutesAgo.replace("<x>", "" + minutes);
} else if (hours < 24) {
if (hours === 1) {
return T.global.time.oneHourAgo;
}
return T.global.time.xHoursAgo.replace("<x>", "" + hours);
} else {
if (days === 1) {
return T.global.time.oneDayAgo;
}
return T.global.time.xDaysAgo.replace("<x>", "" + 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("<seconds>", "" + secs);
} else if (secs < 60 * 60) {
const minutes = Math.floor(secs / 60);
const seconds = secs % 60;
return trans.minutesAndSecondsShort
.replace("<seconds>", "" + seconds)
.replace("<minutes>", "" + minutes);
} else {
const hours = Math.floor(secs / 3600);
const minutes = Math.floor(secs / 60) % 60;
return trans.hoursAndMinutesShort.replace("<minutes>", "" + minutes).replace("<hours>", "" + 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(
"<x>",
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<number>} 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<number>} originalMatrix
* @returns {Object<number, Array<number>>}
*/
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 <link> tag
* @param {string} translation
* @param {string} link
*/
export function fillInLinkIntoTranslation(translation, link) {
return translation
.replace("<link>", "<a href='" + link + "' target='_blank'>")
.replace("</link>", "</a>");
}
/**
* 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;
}