|
|
|
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
|
|
|
|
*/
|
Mod Support - 1.5.0 Update (#1361)
* initial modloader draft
* modloader features
* Refactor mods to use signals
* Add support for modifying and registering new transltions
* Minor adjustments
* Support for string building ids for mods
* Initial support for adding new buildings
* Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading
* Lazy Load mods to make sure all dependencies are loaded
* Expose all exported members automatically to mods
* Fix duplicate exports
* Allow loading mods from standalone
* update changelog
* Fix mods folder incorrect path
* Fix modloading in standalone
* Fix sprites not getting replaced, update demo mod
* Load dev mod via raw loader
* Improve mod developing so mods are directly ready to be deployed, load mods from local file server
* Proper mods ui
* Allow mods to register game systems and draw stuff
* Change mods path
* Fix sprites not loading
* Minor adjustments, closes #1333
* Add support for loading atlases via mods
* Add support for loading mods from external sources in DEV
* Add confirmation when loading mods
* Fix circular dependency
* Minor Keybindings refactor, add support for keybindings to mods, add support for dialogs to mods
* Add some mod signals
* refactor game loading states
* Make shapez exports global
* Start to make mods safer
* Refactor file system electron event handling
* Properly isolate electron renderer process
* Update to latest electron
* Show errors when loading mods
* Update confirm dialgo
* Minor restructure, start to add mod examples
* Allow adding custom themesw
* Add more examples and allow defining custom item processor operations
* Add interface to register new buildings
* Fixed typescript type errors (#1335)
* Refactor building registry, make it easier for mods to add new buildings
* Allow overriding existing methods
* Add more examples and more features
* More mod examples
* Make mod loading simpler
* Add example how to add custom drawings
* Remove unused code
* Minor modloader adjustments
* Support for rotation variants in mods (was broken previously)
* Allow mods to replace builtin sub shapes
* Add helper methods to extend classes
* Fix menu bar on mac os
* Remember window state
* Add support for paste signals
* Add example how to add custom components and systems
* Support for mod settings
* Add example for adding a new item type
* Update class extensions
* Minor adjustments
* Fix typo
* Add notification blocks mod example
* Add small tutorial
* Update readme
* Add better instructions
* Update JSDoc for Replacing Methods (#1336)
* upgraded types for overriding methods
* updated comments
Co-authored-by: Edward Badel <you@example.com>
* Direction lock now indicates when there is a building inbetween
* Fix mod examples
* Fix linter error
* Game state register (#1341)
* Added a gamestate register helper
Added a gamestate register helper
* Update mod_interface.js
* export build options
* Fix runBeforeMethod and runAfterMethod
* Minor game system code cleanup
* Belt path drawing optimization
* Fix belt path optimization
* Belt drawing improvements, again
* Do not render belts in statics disabled view
* Allow external URL to load more than one mod (#1337)
* Allow external URL to load more than one mod
Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though.
* Update modloader.js
* Prettier fixes
* Added link to create-shapezio-mod npm page (#1339)
Added link to create-shapezio-mod npm page: https://www.npmjs.com/package/create-shapezio-mod
* allow command line switch to load more than one mod (#1342)
* Fixed class handle type (#1345)
* Fixed class handle type
* Fixed import game state
* Minor adjustments
* Refactor item acceptor to allow only single direction slots
* Allow specifying minimumGameVersion
* Add sandbox example
* Replaced concatenated strings with template literals (#1347)
* Mod improvements
* Make wired pins component optional on the storage
* Fix mod examples
* Bind `this` for method overriding JSDoc (#1352)
* fix entity debugger reaching HTML elements (#1353)
* Store mods in savegame and show warning when it differs
* Closes #1357
* Fix All Shapez Exports Being Const (#1358)
* Allowed setting of variables inside webpack modules
* remove console log
* Fix stringification of things inside of eval
Co-authored-by: Edward Badel <you@example.com>
* Fix building placer intersection warning
* Add example for storing data in the savegame
* Fix double painter bug (#1349)
* Add example on how to extend builtin buildings
* update readme
* Disable steam achievements when playing with mods
* Update translations
Co-authored-by: Thomas (DJ1TJOO) <44841260+DJ1TJOO@users.noreply.github.com>
Co-authored-by: Bagel03 <70449196+Bagel03@users.noreply.github.com>
Co-authored-by: Edward Badel <you@example.com>
Co-authored-by: Emerald Block <69981203+EmeraldBlock@users.noreply.github.com>
Co-authored-by: saile515 <63782477+saile515@users.noreply.github.com>
Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
2 years ago
|
|
|
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;
|
|
|
|
}
|