1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-11 09:11:50 +00:00
tobspr_shapez.io/src/js/core/utils.js
Даниїл Григор'єв fc33cc2fbf
Add dedicated saves storage, new fs job types
Keep track of the storage ID in each renderer Storage instance and pass
it to the IPC bridge. Jobs are dispatched to the relevant handler (only
saves/ for now) and all (de)compression is handled there.

Add dedicated fs-job types to read or write and (de)compress data
from/to the file picked by the user. Remove redundant utility functions
that used web APIs instead.
2025-04-15 01:57:00 +03:00

681 lines
17 KiB
JavaScript

import { T } from "../translations";
const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"];
/**
* Returns a platform name
* @returns {"standalone"}
*/
export function getPlatformName() {
return "standalone";
}
/**
* 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 Math.floor(Math.random() * (end - start + 1) + start);
}
/**
* 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) {
const range = document.createRange();
range.selectNodeContents(elem);
range.deleteContents();
}
}
/**
* Returns if the game supports this browser
*/
export function isSupportedBrowser() {
return true;
}
/**
* 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>");
}
const MAX_ROMAN_NUMBER = 49;
const romanLiteralsCache = ["0"];
/**
*
* @param {number} number
* @returns {string}
*/
export function getRomanNumber(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;
}
/**
* Returns the appropriate logo sprite path
*/
export function getLogoSprite() {
return "logo.png";
}
/**
* Rejects a promise after X ms
* @param {Promise} promise
*/
export function timeoutPromise(promise, timeout = 30000) {
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => reject("timeout of " + timeout + " ms exceeded"), timeout);
}),
promise,
]);
}