Trying things out with emscripten
super inefficient for dev for now; set multiple files to compile separately into frankenstein js files as I migrate code
This commit is contained in:
parent
a9c96d3629
commit
b157f79fc0
9 changed files with 3206 additions and 0 deletions
17
js/c++/Makefile
Normal file
17
js/c++/Makefile
Normal file
|
@ -0,0 +1,17 @@
|
|||
CXX=emcc
|
||||
CXXFLAGS=-sENVIRONMENT=web -sSINGLE_FILE -sMODULARIZE -sWASM_ASYNC_COMPILATION=0 -lembind --closure 1
|
||||
|
||||
all: utils.js powders.js
|
||||
|
||||
%.js: %.cpp
|
||||
$(CXX) $(CXXFLAGS) -sEXPORT_NAME=create_$* -o $@.out $<
|
||||
cat $@.out $@.in > ../$@
|
||||
|
||||
powders.js: powders.cpp
|
||||
|
||||
utils.js: utils.cpp
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f utils.js.out powders.js.out
|
||||
|
12
js/c++/powders.cpp
Normal file
12
js/c++/powders.cpp
Normal file
|
@ -0,0 +1,12 @@
|
|||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/bind.h>
|
||||
using namespace emscripten;
|
||||
#endif
|
||||
|
||||
#include "powders.h"
|
||||
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EMSCRIPTEN_BINDINGS(powders) {
|
||||
}
|
||||
#endif
|
1
js/c++/powders.h
Normal file
1
js/c++/powders.h
Normal file
|
@ -0,0 +1 @@
|
|||
#pragma once
|
237
js/c++/powders.js.in
Normal file
237
js/c++/powders.js.in
Normal file
|
@ -0,0 +1,237 @@
|
|||
|
||||
const _module_powders = create_powders();
|
||||
|
||||
let powderIDs = new Map();
|
||||
let powderNames = new Map();
|
||||
let _powderID = 0;
|
||||
for (const x of skp_elements) {
|
||||
for (let i = 1; i <= 6; ++i) {
|
||||
// Support both upper and lowercase, I guess.
|
||||
powderIDs.set(x.toUpperCase()+i, _powderID);
|
||||
powderIDs.set(x+i, _powderID);
|
||||
powderNames.set(_powderID, x+i);
|
||||
_powderID++;
|
||||
}
|
||||
}
|
||||
|
||||
// Ordering: [dmgMin, dmgMax, convert, defPlus, defMinus (+6 mod 5)]
|
||||
class Powder {
|
||||
constructor(min, max, convert, defPlus, defMinus) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.convert = convert;
|
||||
this.defPlus = defPlus;
|
||||
this.defMinus = defMinus;
|
||||
}
|
||||
}
|
||||
function _p(a,b,c,d,e) { return new Powder(a,b,c,d,e); } //bruh moment
|
||||
|
||||
let powderStats = [
|
||||
_p(3,6,17,2,1), _p(5,8,21,4,2), _p(6,10,25,8,3), _p(7,10,31,14,5), _p(9,11,38,22,9), _p(11,13,46,30,13),
|
||||
_p(1,8,9,3,1), _p(1,12,11,5,1), _p(2,15,13,9,2), _p(3,15,17,14,4), _p(4,17,22,20,7), _p(5,20,28,28,10),
|
||||
_p(3,4,13,3,1), _p(4,6,15,6,1), _p(5,8,17,11,2), _p(6,8,21,18,4), _p(7,10,26,28,7), _p(9,11,32,40,10),
|
||||
_p(2,5,14,3,1), _p(4,8,16,5,2), _p(5,9,19,9,3), _p(6,9,24,16,5), _p(8,10,30,25,9), _p(10,12,37,36,13),
|
||||
_p(2,6,11,3,1), _p(3,10,14,6,2), _p(4,11,17,10,3), _p(5,11,22,16,5), _p(7,12,28,24,9), _p(8,14,35,34,13)
|
||||
];
|
||||
|
||||
class PowderIngredient {
|
||||
constructor(durability, skpReq) {
|
||||
this.durability = durability;
|
||||
this.skpReq = skpReq;
|
||||
}
|
||||
}
|
||||
function _pi(a,b) { return new PowderIngredient(a,b)}
|
||||
|
||||
let powderIngreds = [
|
||||
_pi(-35,0),_pi(-52.5,0),_pi(-70,10),_pi(-91,20),_pi(-112,28),_pi(-133,36)
|
||||
];
|
||||
//Ordering: [weapon special name, weapon special effects, armor special name, armor special effects]
|
||||
class PowderSpecial{
|
||||
constructor(wSpName, wSpEff, aSpName, aSpEff, cap){
|
||||
this.weaponSpecialName = wSpName;
|
||||
this.weaponSpecialEffects = wSpEff;
|
||||
this.armorSpecialName = aSpName;
|
||||
this.armorSpecialEffects = aSpEff;
|
||||
this.cap = cap;
|
||||
}
|
||||
}
|
||||
function _ps(a,b,c,d,e) { return new PowderSpecial(a,b,c,d,e); } //bruh moment
|
||||
|
||||
let powderSpecialStats = [
|
||||
_ps("Quake",new Map([["Radius",[4.5,5,5.5,6,6.5]], ["Damage",[240,280,320,360,400]] ]),"Rage",new Map([ ["Damage", [0.2,0.4,0.6,0.8,1.0]],["Description", "% " + "\u2764" + " Missing below 75%"] ]),300), //e
|
||||
_ps("Chain Lightning",new Map([ ["Chains", [5,6,7,8,9]], ["Damage", [200,225,250,275,300]] ]),"Kill Streak",new Map([ ["Damage", [3,4.5,6,7.5,9]],["Duration", [5,5,5,5,5]],["Description", "Mob Killed"] ]),150), //t
|
||||
_ps("Curse",new Map([ ["Duration", [4,4,4,4,4]],["Damage Boost", [10,15,20,25,30]] ]),"Concentration",new Map([ ["Damage", [0.05,0.1,0.15,0.2,0.25]],["Duration",[1,1,1,1,1]],["Description", "Mana Used"] ]),100), //w
|
||||
_ps("Courage",new Map([ ["Duration", [4,4,4,4,4]],["Damage", [60, 70, 80, 90, 100]],["Damage Boost", [10,12.5,15,17.5,20]] ]),"Endurance",new Map([ ["Damage", [2,3,4,5,6]],["Duration", [8,8,8,8,8]],["Description", "Hit Taken"] ]),100), //f
|
||||
_ps("Wind Prison",new Map([ ["Duration", [3,3.5,4,4.5,5]],["Damage Boost", [100,125,150,175,200]],["Knockback", [8,12,16,20,24]] ]),"Dodge",new Map([ ["Damage",[2,3,4,5,6]],["Duration",[2,3,4,5,6]],["Description","Near Mobs"] ]),150) //a
|
||||
];
|
||||
|
||||
/**
|
||||
* Apply armor powders.
|
||||
* Encoding shortcut assumes that all powders give +def to one element
|
||||
* and -def to the element "behind" it in cycle ETWFA, which is true
|
||||
* as of now and unlikely to change in the near future.
|
||||
*/
|
||||
function applyArmorPowders(expandedItem) {
|
||||
const powders = expandedItem.get('powders');
|
||||
for(const id of powders){
|
||||
let powder = powderStats[id];
|
||||
let name = powderNames.get(id).charAt(0);
|
||||
let prevName = skp_elements[(skp_elements.indexOf(name) + 4 )% 5];
|
||||
expandedItem.set(name+"Def", (expandedItem.get(name+"Def") || 0) + powder["defPlus"]);
|
||||
expandedItem.set(prevName+"Def", (expandedItem.get(prevName+"Def") || 0) - powder["defMinus"]);
|
||||
}
|
||||
}
|
||||
|
||||
const damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ];
|
||||
const damage_present_key = 'damagePresent';
|
||||
/**
|
||||
* Apply weapon powders. MUTATES THE ITEM!
|
||||
* Adds entries for `damage_keys` and `damage_present_key`
|
||||
* For normal items, `damage_keys` is 6x2 list (elem: [min, max])
|
||||
* For crafted items, `damage_keys` is 6x2x2 list (elem: [minroll: [min, max], maxroll: [min, max]])
|
||||
*/
|
||||
function apply_weapon_powders(item) {
|
||||
let present;
|
||||
if (item.get("tier") !== "Crafted") {
|
||||
let weapon_result = calc_weapon_powder(item);
|
||||
let damages = weapon_result[0];
|
||||
present = weapon_result[1];
|
||||
for (const i in damage_keys) {
|
||||
item.set(damage_keys[i], damages[i]);
|
||||
}
|
||||
} else {
|
||||
let base_low = [item.get("nDamBaseLow"),item.get("eDamBaseLow"),item.get("tDamBaseLow"),item.get("wDamBaseLow"),item.get("fDamBaseLow"),item.get("aDamBaseLow")];
|
||||
let results_low = calc_weapon_powder(item, base_low);
|
||||
let damage_low = results_low[0];
|
||||
let base_high = [item.get("nDamBaseHigh"),item.get("eDamBaseHigh"),item.get("tDamBaseHigh"),item.get("wDamBaseHigh"),item.get("fDamBaseHigh"),item.get("aDamBaseHigh")];
|
||||
let results_high = calc_weapon_powder(item, base_high);
|
||||
let damage_high = results_high[0];
|
||||
present = results_high[1];
|
||||
|
||||
for (const i in damage_keys) {
|
||||
item.set(damage_keys[i], [damage_low[i], damage_high[i]]);
|
||||
}
|
||||
}
|
||||
item.set(damage_present_key, present);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weapon damage from powder.
|
||||
*
|
||||
* Params:
|
||||
* weapon: Weapon to apply powder to
|
||||
* damageBases: used by crafted
|
||||
*
|
||||
* Return:
|
||||
* [damages, damage_present]
|
||||
*/
|
||||
function calc_weapon_powder(weapon, damageBases) {
|
||||
let powders = weapon.get("powders").slice();
|
||||
|
||||
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
|
||||
let damages = [
|
||||
weapon.get('nDam').split('-').map(Number),
|
||||
weapon.get('eDam').split('-').map(Number),
|
||||
weapon.get('tDam').split('-').map(Number),
|
||||
weapon.get('wDam').split('-').map(Number),
|
||||
weapon.get('fDam').split('-').map(Number),
|
||||
weapon.get('aDam').split('-').map(Number)
|
||||
];
|
||||
|
||||
// Applying spell conversions
|
||||
let neutralBase = damages[0].slice();
|
||||
let neutralRemainingRaw = damages[0].slice();
|
||||
|
||||
//powder application for custom crafted weapons is inherently fucked because there is no base. Unsure what to do.
|
||||
|
||||
//Powder application for Crafted weapons - this implementation is RIGHT YEAAAAAAAAA
|
||||
//1st round - apply each as ingred, 2nd round - apply as normal
|
||||
if (weapon.get("tier") === "Crafted" && !weapon.get("custom")) {
|
||||
for (const p of powders.concat(weapon.get("ingredPowders"))) {
|
||||
let powder = powderStats[p]; //use min, max, and convert
|
||||
let element = Math.floor((p+0.01)/6); //[0,4], the +0.01 attempts to prevent division error
|
||||
let diff = Math.floor(damageBases[0] * powder.convert/100);
|
||||
damageBases[0] -= diff;
|
||||
damageBases[element+1] += diff + Math.floor( (powder.min + powder.max) / 2 );
|
||||
}
|
||||
//update all damages
|
||||
for (let i = 0; i < damages.length; i++) {
|
||||
damages[i] = [Math.floor(damageBases[i] * 0.9), Math.floor(damageBases[i] * 1.1)];
|
||||
}
|
||||
neutralRemainingRaw = damages[0].slice();
|
||||
neutralBase = damages[0].slice();
|
||||
}
|
||||
|
||||
//apply powders to weapon (1.21 fked implementation)
|
||||
let powder_apply_order = [];
|
||||
let powder_apply_map = new Map();
|
||||
for (const powderID of powders) {
|
||||
const powder = powderStats[powderID];
|
||||
// Bitwise to force conversion to integer (integer division).
|
||||
const element = (powderID/6) | 0;
|
||||
const conversion_ratio = powder.convert/100;
|
||||
|
||||
if (powder_apply_map.has(element)) {
|
||||
let apply_info = powder_apply_map.get(element);
|
||||
apply_info.conv += conversion_ratio;
|
||||
apply_info.min += powder.min;
|
||||
apply_info.max += powder.max;
|
||||
}
|
||||
else {
|
||||
let apply_info = {
|
||||
conv: conversion_ratio,
|
||||
min: powder.min,
|
||||
max: powder.max
|
||||
};
|
||||
powder_apply_order.push(element);
|
||||
powder_apply_map.set(element, apply_info);
|
||||
}
|
||||
}
|
||||
for (const element of powder_apply_order) {
|
||||
const apply_info = powder_apply_map.get(element);
|
||||
const conversion_ratio = apply_info.conv;
|
||||
const min_diff = Math.min(neutralRemainingRaw[0], conversion_ratio * neutralRemainingRaw[0]);
|
||||
const max_diff = Math.min(neutralRemainingRaw[1], conversion_ratio * neutralRemainingRaw[1]);
|
||||
neutralRemainingRaw[0] -= min_diff;
|
||||
neutralRemainingRaw[1] -= max_diff;
|
||||
damages[element+1][0] += min_diff;
|
||||
damages[element+1][1] += max_diff;
|
||||
damages[element+1][0] += apply_info.min;
|
||||
damages[element+1][1] += apply_info.max;
|
||||
}
|
||||
|
||||
/*
|
||||
//apply powders to weapon
|
||||
for (const powderID of powders) {
|
||||
const powder = powderStats[powderID];
|
||||
// Bitwise to force conversion to integer (integer division).
|
||||
const element = (powderID/6) | 0;
|
||||
let conversionRatio = powder.convert/100;
|
||||
if (neutralRemainingRaw[1] > 0) {
|
||||
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);
|
||||
let max_diff = Math.min(neutralRemainingRaw[1], conversionRatio * neutralBase[1]);
|
||||
|
||||
damages[element+1][0] = Math.floor(round_near(damages[element+1][0] + min_diff));
|
||||
damages[element+1][1] = Math.floor(round_near(damages[element+1][1] + max_diff));
|
||||
neutralRemainingRaw[0] = Math.floor(round_near(neutralRemainingRaw[0] - min_diff));
|
||||
neutralRemainingRaw[1] = Math.floor(round_near(neutralRemainingRaw[1] - max_diff));
|
||||
//damages[element+1][0] += min_diff;
|
||||
//damages[element+1][1] += max_diff;
|
||||
//neutralRemainingRaw[0] -= min_diff;
|
||||
//neutralRemainingRaw[1] -= max_diff;
|
||||
}
|
||||
damages[element+1][0] += powder.min;
|
||||
damages[element+1][1] += powder.max;
|
||||
}
|
||||
*/
|
||||
|
||||
// The ordering of these two blocks decides whether neutral is present when converted away or not.
|
||||
damages[0] = neutralRemainingRaw;
|
||||
|
||||
// The ordering of these two blocks decides whether neutral is present when converted away or not.
|
||||
let present_elements = []
|
||||
for (const damage of damages) {
|
||||
present_elements.push(damage[1] > 0);
|
||||
}
|
||||
return [damages, present_elements];
|
||||
}
|
859
js/c++/powders.js.out
Normal file
859
js/c++/powders.js.out
Normal file
File diff suppressed because one or more lines are too long
50
js/c++/utils.cpp
Normal file
50
js/c++/utils.cpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/bind.h>
|
||||
using namespace emscripten;
|
||||
#endif
|
||||
|
||||
#include "utils.h"
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* Clamp number between low and high values.
|
||||
*
|
||||
* @param num: value to clamp
|
||||
* @param low
|
||||
* @param high
|
||||
*
|
||||
* @return clamped value
|
||||
*/
|
||||
float clamp(float num, float low, float high) {
|
||||
return std::min(std::max(num, low), high);
|
||||
}
|
||||
|
||||
// Permutations in js reference (also cool algorithm):
|
||||
// https://stackoverflow.com/a/41068709
|
||||
//function perm(a){
|
||||
// if (a.length == 0) return [[]];
|
||||
// var r = [[a[0]]],
|
||||
// t = [],
|
||||
// s = [];
|
||||
// if (a.length == 1) return r;
|
||||
// for (var i = 1, la = a.length; i < la; i++){
|
||||
// for (var j = 0, lr = r.length; j < lr; j++){
|
||||
// r[j].push(a[i]);
|
||||
// t.push(r[j]);
|
||||
// for(var k = 1, lrj = r[j].length; k < lrj; k++){
|
||||
// for (var l = 0; l < lrj; l++) s[l] = r[j][(k+l)%lrj];
|
||||
// t[t.length] = s;
|
||||
// s = [];
|
||||
// }
|
||||
// }
|
||||
// r = t;
|
||||
// t = [];
|
||||
// }
|
||||
// return r;
|
||||
//}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EMSCRIPTEN_BINDINGS(utils) {
|
||||
function("clamp", &clamp);
|
||||
}
|
||||
#endif
|
12
js/c++/utils.h
Normal file
12
js/c++/utils.h
Normal file
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
/**
|
||||
* Clamp number between low and high values.
|
||||
*
|
||||
* @param num: value to clamp
|
||||
* @param low
|
||||
* @param high
|
||||
*
|
||||
* @return clamped value
|
||||
*/
|
||||
float clamp(float num, float low, float high);
|
999
js/c++/utils.js.in
Normal file
999
js/c++/utils.js.in
Normal file
|
@ -0,0 +1,999 @@
|
|||
let getUrl = window.location;
|
||||
const url_base = getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.split('/')[1];
|
||||
|
||||
const _module_utils = create_utils();
|
||||
const clamp = _module_utils.clamp;
|
||||
|
||||
// huge regex :doom:
|
||||
// replace with navigator.userAgentData.mobile once it has wider support
|
||||
const isMobile = function() {
|
||||
let check = false;
|
||||
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
|
||||
return check;
|
||||
}(); // runs immediately, so mobileCheck is a boolean not a function
|
||||
|
||||
const zip2 = (a, b) => a.map((k, i) => [k, b[i]]);
|
||||
const zip3 = (a, b, c) => a.map((k, i) => [k, b[i], c[i]]);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Permutations in js reference (also cool algorithm):
|
||||
// https://stackoverflow.com/a/41068709
|
||||
function perm(a){
|
||||
if (a.length == 0) return [[]];
|
||||
var r = [[a[0]]],
|
||||
t = [],
|
||||
s = [];
|
||||
if (a.length == 1) return r;
|
||||
for (var i = 1, la = a.length; i < la; i++){
|
||||
for (var j = 0, lr = r.length; j < lr; j++){
|
||||
r[j].push(a[i]);
|
||||
t.push(r[j]);
|
||||
for(var k = 1, lrj = r[j].length; k < lrj; k++){
|
||||
for (var l = 0; l < lrj; l++) s[l] = r[j][(k+l)%lrj];
|
||||
t[t.length] = s;
|
||||
s = [];
|
||||
}
|
||||
}
|
||||
r = t;
|
||||
t = [];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function round_near(value) {
|
||||
let eps = 0.00000001;
|
||||
if (Math.abs(value - Math.round(value)) < eps) {
|
||||
return Math.round(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function setText(id, text) {
|
||||
document.getElementById(id).textContent = text;
|
||||
}
|
||||
|
||||
function setHTML(id, html) {
|
||||
document.getElementById(id).innerHTML = html;
|
||||
}
|
||||
|
||||
function setValue(id, value) {
|
||||
let el = document.getElementById(id);
|
||||
if (el == null) {
|
||||
console.log("WARN tried to set text value of id {"+id+"} to ["+value+"] but did not exist!");
|
||||
return;
|
||||
}
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
|
||||
function log(b, n) {
|
||||
return Math.log(n) / Math.log(b);
|
||||
}
|
||||
|
||||
// Base 64 encoding tools
|
||||
// https://stackoverflow.com/a/27696695
|
||||
// Modified for fixed precision
|
||||
|
||||
// Base64.fromInt(-2147483648); // gives "200000"
|
||||
// Base64.toInt("200000"); // gives -2147483648
|
||||
Base64 = (function () {
|
||||
var digitsStr =
|
||||
// 0 8 16 24 32 40 48 56 63
|
||||
// v v v v v v v v v
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-";
|
||||
var digits = digitsStr.split('');
|
||||
var digitsMap = {};
|
||||
for (var i = 0; i < digits.length; i++) {
|
||||
digitsMap[digits[i]] = i;
|
||||
}
|
||||
return {
|
||||
fromIntV: function(int32) {
|
||||
var result = '';
|
||||
while (true) {
|
||||
result = digits[int32 & 0x3f] + result;
|
||||
int32 >>>= 6;
|
||||
if (int32 === 0)
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
fromIntN: function(int32, n) {
|
||||
var result = '';
|
||||
for (let i = 0; i < n; ++i) {
|
||||
result = digits[int32 & 0x3f] + result;
|
||||
int32 >>= 6;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
toInt: function(digitsStr) {
|
||||
var result = 0;
|
||||
var digits = digitsStr.split('');
|
||||
for (var i = 0; i < digits.length; i++) {
|
||||
result = (result << 6) + digitsMap[digits[i]];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
toIntSigned: function(digitsStr) {
|
||||
var result = 0;
|
||||
var digits = digitsStr.split('');
|
||||
if (digits[0] && (digitsMap[digits[0]] & 0x20)) {
|
||||
result = -1;
|
||||
}
|
||||
for (var i = 0; i < digits.length; i++) {
|
||||
result = (result << 6) + digitsMap[digits[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
/** A class used to represent an arbitrary length bit vector. Very useful for encoding and decoding.
|
||||
*
|
||||
*/
|
||||
class BitVector {
|
||||
|
||||
/** Constructs an arbitrary-length bit vector.
|
||||
* @class
|
||||
* @param {String | Number} data - The data to append.
|
||||
* @param {Number} length - A set length for the data. Ignored if data is a string.
|
||||
*
|
||||
* The structure of the Uint32Array should be [[last, ..., first], ..., [last, ..., first], [empty space, last, ..., first]]
|
||||
*/
|
||||
constructor(data, length) {
|
||||
let bit_vec = [];
|
||||
|
||||
if (typeof data === "string") {
|
||||
let int = 0;
|
||||
let bv_idx = 0;
|
||||
length = data.length * 6;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let char = Base64.toInt(data[i]);
|
||||
let pre_pos = bv_idx % 32;
|
||||
int |= (char << bv_idx);
|
||||
bv_idx += 6;
|
||||
let post_pos = bv_idx % 32;
|
||||
if (post_pos < pre_pos) { //we have to have filled up the integer
|
||||
bit_vec.push(int);
|
||||
int = (char >>> (6 - post_pos));
|
||||
}
|
||||
|
||||
if (i == data.length - 1 && post_pos != 0) {
|
||||
bit_vec.push(int);
|
||||
}
|
||||
}
|
||||
} else if (typeof data === "number") {
|
||||
if (typeof length === "undefined")
|
||||
if (length < 0) {
|
||||
throw new RangeError("BitVector must have nonnegative length.");
|
||||
}
|
||||
|
||||
//convert to int just in case
|
||||
data = Math.round(data);
|
||||
|
||||
//range of numbers that won't fit in a uint32
|
||||
if (data > 2**32 - 1 || data < -(2 ** 32 - 1)) {
|
||||
throw new RangeError("Numerical data has to fit within a 32-bit integer range to instantiate a BitVector.");
|
||||
}
|
||||
bit_vec.push(data);
|
||||
} else {
|
||||
throw new TypeError("BitVector must be instantiated with a Number or a B64 String");
|
||||
}
|
||||
|
||||
this.length = length;
|
||||
this.bits = new Uint32Array(bit_vec);
|
||||
}
|
||||
|
||||
/** Return value of bit at index idx.
|
||||
*
|
||||
* @param {Number} idx - The index to read
|
||||
*
|
||||
* @returns The bit value at position idx
|
||||
*/
|
||||
read_bit(idx) {
|
||||
if (idx < 0 || idx >= this.length) {
|
||||
throw new RangeError("Cannot read bit outside the range of the BitVector. ("+idx+" > "+this.length+")");
|
||||
}
|
||||
return ((this.bits[Math.floor(idx / 32)] & (1 << idx)) == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
/** Returns an integer value (if possible) made from the range of bits [start, end). Undefined behavior if the range to read is too big.
|
||||
*
|
||||
* @param {Number} start - The index to start slicing from. Inclusive.
|
||||
* @param {Number} end - The index to end slicing at. Exclusive.
|
||||
*
|
||||
* @returns An integer representation of the sliced bits.
|
||||
*/
|
||||
slice(start, end) {
|
||||
//TO NOTE: JS shifting is ALWAYS in mod 32. a << b will do a << (b mod 32) implicitly.
|
||||
|
||||
if (end < start) {
|
||||
throw new RangeError("Cannot slice a range where the end is before the start.");
|
||||
} else if (end == start) {
|
||||
return 0;
|
||||
} else if (end - start > 32) {
|
||||
//requesting a slice of longer than 32 bits (safe integer "length")
|
||||
throw new RangeError("Cannot slice a range of longer than 32 bits (unsafe to store in an integer).");
|
||||
}
|
||||
|
||||
let res = 0;
|
||||
if (Math.floor((end - 1) / 32) == Math.floor(start / 32)) {
|
||||
//the range is within 1 uint32 section - do some relatively fast bit twiddling
|
||||
res = (this.bits[Math.floor(start / 32)] & ~((((~0) << ((end - 1))) << 1) | ~((~0) << (start)))) >>> (start % 32);
|
||||
} else {
|
||||
//the number of bits in the uint32s
|
||||
let start_pos = (start % 32);
|
||||
let int_idx = Math.floor(start/32);
|
||||
res = (this.bits[int_idx] & ((~0) << (start))) >>> (start_pos);
|
||||
res |= (this.bits[int_idx + 1] & ~((~0) << (end))) << (32 - start_pos);
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
// General code - slow
|
||||
// for (let i = start; i < end; i++) {
|
||||
// res |= (get_bit(i) << (i - start));
|
||||
// }
|
||||
}
|
||||
|
||||
/** Assign bit at index idx to 1.
|
||||
*
|
||||
* @param {Number} idx - The index to set.
|
||||
*/
|
||||
set_bit(idx) {
|
||||
if (idx < 0 || idx >= this.length) {
|
||||
throw new RangeError("Cannot set bit outside the range of the BitVector.");
|
||||
}
|
||||
this.bits[Math.floor(idx / 32)] |= (1 << idx % 32);
|
||||
}
|
||||
|
||||
/** Assign bit at index idx to 0.
|
||||
*
|
||||
* @param {Number} idx - The index to clear.
|
||||
*/
|
||||
clear_bit(idx) {
|
||||
if (idx < 0 || idx >= this.length) {
|
||||
throw new RangeError("Cannot clear bit outside the range of the BitVector.");
|
||||
}
|
||||
this.bits[Math.floor(idx / 32)] &= ~(1 << idx % 32);
|
||||
}
|
||||
|
||||
/** Creates a string version of the bit vector in B64. Does not keep the order of elements a sensible human readable format.
|
||||
*
|
||||
* @returns A b64 string representation of the BitVector.
|
||||
*/
|
||||
toB64() {
|
||||
if (this.length == 0) {
|
||||
return "";
|
||||
}
|
||||
let b64_str = "";
|
||||
let i = 0;
|
||||
while (i < this.length) {
|
||||
b64_str += Base64.fromIntV(this.slice(i, i + 6), 1);
|
||||
i += 6;
|
||||
}
|
||||
|
||||
return b64_str;
|
||||
}
|
||||
|
||||
/** Returns a BitVector in bitstring format. Probably only useful for dev debugging.
|
||||
*
|
||||
* @returns A bit string representation of the BitVector. Goes from higher-indexed bits to lower-indexed bits. (n ... 0)
|
||||
*/
|
||||
toString() {
|
||||
let ret_str = "";
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
ret_str = (this.read_bit(i) == 0 ? "0": "1") + ret_str;
|
||||
}
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
/** Returns a BitVector in bitstring format. Probably only useful for dev debugging.
|
||||
*
|
||||
* @returns A bit string representation of the BitVector. Goes from lower-indexed bits to higher-indexed bits. (0 ... n)
|
||||
*/
|
||||
toStringR() {
|
||||
let ret_str = "";
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
ret_str += (this.read_bit(i) == 0 ? "0": "1");
|
||||
}
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
/** Appends data to the BitVector.
|
||||
*
|
||||
* @param {Number | String} data - The data to append.
|
||||
* @param {Number} length - The length, in bits, of the new data. This is ignored if data is a string.
|
||||
*/
|
||||
append(data, length) {
|
||||
if (length < 0) {
|
||||
throw new RangeError("BitVector length must increase by a nonnegative number.");
|
||||
}
|
||||
|
||||
let bit_vec = [];
|
||||
for (const uint of this.bits) {
|
||||
bit_vec.push(uint);
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
let int = bit_vec[bit_vec.length - 1];
|
||||
let bv_idx = this.length;
|
||||
length = data.length * 6;
|
||||
let updated_curr = false;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let char = Base64.toInt(data[i]);
|
||||
let pre_pos = bv_idx % 32;
|
||||
int |= (char << bv_idx);
|
||||
bv_idx += 6;
|
||||
let post_pos = bv_idx % 32;
|
||||
if (post_pos < pre_pos) { //we have to have filled up the integer
|
||||
if (bit_vec.length == this.bits.length && !updated_curr) {
|
||||
bit_vec[bit_vec.length - 1] = int;
|
||||
updated_curr = true;
|
||||
} else {
|
||||
bit_vec.push(int);
|
||||
}
|
||||
int = (char >>> (6 - post_pos));
|
||||
}
|
||||
|
||||
if (i == data.length - 1) {
|
||||
if (bit_vec.length == this.bits.length && !updated_curr) {
|
||||
bit_vec[bit_vec.length - 1] = int;
|
||||
} else if (post_pos != 0) {
|
||||
bit_vec.push(int);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof data === "number") {
|
||||
//convert to int just in case
|
||||
let int = Math.round(data);
|
||||
|
||||
//range of numbers that "could" fit in a uint32 -> [0, 2^32) U [-2^31, 2^31)
|
||||
if (data > 2**32 - 1 || data < -(2 ** 31)) {
|
||||
throw new RangeError("Numerical data has to fit within a 32-bit integer range to instantiate a BitVector.");
|
||||
}
|
||||
//could be split between multiple new ints
|
||||
//reminder that shifts implicitly mod 32
|
||||
bit_vec[bit_vec.length - 1] |= ((int & ~((~0) << length)) << (this.length));
|
||||
if (((this.length - 1) % 32 + 1) + length > 32) {
|
||||
bit_vec.push(int >>> (32 - this.length));
|
||||
}
|
||||
} else {
|
||||
throw new TypeError("BitVector must be appended with a Number or a B64 String");
|
||||
}
|
||||
|
||||
this.bits = new Uint32Array(bit_vec);
|
||||
this.length += length;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Turns a raw stat and a % stat into a final stat on the basis that - raw and >= 100% becomes 0 and + raw and <=-100% becomes negative.
|
||||
Pct would be 0.80 for 80%, -1.20 for 120%, etc
|
||||
Example Outputs:
|
||||
raw: -100
|
||||
pct: +0.20, output = -80
|
||||
pct: +1.20, output = 0
|
||||
pct: -0.20, output = -120
|
||||
pct: -1.20, output = -220
|
||||
|
||||
raw: +100
|
||||
pct: +0.20, output = 120
|
||||
pct: +1.20, output = 220
|
||||
pct: -0.20, output = 80
|
||||
pct: -1.20, output = -20
|
||||
*/
|
||||
function rawToPct(raw, pct){
|
||||
final = 0;
|
||||
if (raw < 0){
|
||||
final = (Math.min(0, raw - (raw * pct) ));
|
||||
}else if(raw > 0){
|
||||
final = raw + (raw * pct);
|
||||
}else{ //do nothing - final's already 0
|
||||
}
|
||||
return final;
|
||||
}
|
||||
|
||||
/*
|
||||
* Clipboard utilities
|
||||
* From: https://stackoverflow.com/a/30810322
|
||||
*/
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
var textArea = document.createElement("textarea");
|
||||
|
||||
//
|
||||
// *** This styling is an extra step which is likely not required. ***
|
||||
//
|
||||
// Why is it here? To ensure:
|
||||
// 1. the element is able to have focus and selection.
|
||||
// 2. if the element was to flash render it has minimal visual impact.
|
||||
// 3. less flakyness with selection and copying which **might** occur if
|
||||
// the textarea element is not visible.
|
||||
//
|
||||
// The likelihood is the element won't even render, not even a
|
||||
// flash, so some of these are just precautions. However in
|
||||
// Internet Explorer the element is visible whilst the popup
|
||||
// box asking the user for permission for the web page to
|
||||
// copy to the clipboard.
|
||||
//
|
||||
|
||||
// Place in the top-left corner of screen regardless of scroll position.
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = 0;
|
||||
textArea.style.left = 0;
|
||||
|
||||
// Ensure it has a small width and height. Setting to 1px / 1em
|
||||
// doesn't work as this gives a negative w/h on some browsers.
|
||||
textArea.style.width = '2em';
|
||||
textArea.style.height = '2em';
|
||||
|
||||
// We don't need padding, reducing the size if it does flash render.
|
||||
textArea.style.padding = 0;
|
||||
|
||||
// Clean up any borders.
|
||||
textArea.style.border = 'none';
|
||||
textArea.style.outline = 'none';
|
||||
textArea.style.boxShadow = 'none';
|
||||
|
||||
// Avoid flash of the white box if rendered for any reason.
|
||||
textArea.style.background = 'transparent';
|
||||
|
||||
|
||||
textArea.value = text;
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
var msg = successful ? 'successful' : 'unsuccessful';
|
||||
console.log('Copying text command was ' + msg);
|
||||
} catch (err) {
|
||||
console.log('Oops, unable to copy');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
function copyTextToClipboard(text) {
|
||||
if (!navigator.clipboard) {
|
||||
fallbackCopyTextToClipboard(text);
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
}, function(err) {
|
||||
console.error('Async: Could not copy text: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random color using the #(R)(G)(B) format.
|
||||
*/
|
||||
function randomColor() {
|
||||
return '#' + Math.round(Math.random() * 0xFFFFFF).toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random color, but lightning must be relatively high (>0.5).
|
||||
*
|
||||
* @returns a random color in RGB 6-bit form.
|
||||
*/
|
||||
function randomColorLight() {
|
||||
return randomColorHSL([0,1],[0,1],[0.5,1]);
|
||||
}
|
||||
|
||||
/** Generates a random color given HSL restrictions.
|
||||
*
|
||||
* @returns a random color in RGB 6-bit form.
|
||||
*/
|
||||
function randomColorHSL(h,s,l) {
|
||||
var letters = '0123456789abcdef';
|
||||
let h_var = h[0] + (h[1]-h[0])*Math.random(); //hue
|
||||
let s_var = s[0] + (s[1]-s[0])*Math.random(); //saturation
|
||||
let l_var = l[0] + (l[1]-l[0])*Math.random(); //lightness
|
||||
let rgb = hslToRgb(h_var,s_var,l_var);
|
||||
let color = "#";
|
||||
for (const c of rgb) {
|
||||
color += letters[Math.floor(c/16)] + letters[c%16];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 255]. Not written by wynnbuilder devs.
|
||||
*
|
||||
* @param {number} h The hue
|
||||
* @param {number} s The saturation
|
||||
* @param {number} l The lightness
|
||||
* @return {Array} The RGB representation
|
||||
*/
|
||||
function hslToRgb(h, s, l){
|
||||
var r, g, b;
|
||||
|
||||
if(s == 0){
|
||||
r = g = b = l; // achromatic
|
||||
}else{
|
||||
var hue2rgb = function hue2rgb(p, q, t){
|
||||
if(t < 0) t += 1;
|
||||
if(t > 1) t -= 1;
|
||||
if(t < 1/6) return p + (q - p) * 6 * t;
|
||||
if(t < 1/2) return q;
|
||||
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
/** Creates a tooltip.
|
||||
*
|
||||
* @param {DOM Element} elem - the element to make a tooltip
|
||||
* @param {String} element_type - the HTML element type that the tooltiptext should be.
|
||||
* @param {String} tooltiptext - the text to display in the tooltip.
|
||||
* @param {DOM Element} parent - the parent elem. optional.
|
||||
* @param {String[]} classList - a list of classes to add to the element.
|
||||
*/
|
||||
function createTooltip(elem, element_type, tooltiptext, parent, classList) {
|
||||
elem = document.createElement(element_type);
|
||||
elem.classList.add("tooltiptext");
|
||||
if (tooltiptext.includes("\n")) {
|
||||
let texts = tooltiptext.split("\n");
|
||||
for (const t of texts) {
|
||||
let child = document.createElement(element_type);
|
||||
child.textContent = t;
|
||||
elem.appendChild(child);
|
||||
}
|
||||
} else {
|
||||
elem.textContent = tooltiptext;
|
||||
}
|
||||
for (const c of classList) {
|
||||
elem.classList.add(c);
|
||||
}
|
||||
if (parent) {
|
||||
parent.classList.add("tooltip");
|
||||
parent.appendChild(elem);
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
||||
/** A generic function that toggles the on and off state of a button.
|
||||
*
|
||||
* @param {String} button_id - the id name of the button.
|
||||
*/
|
||||
function toggleButton(button_id) {
|
||||
let elem = document.getElementById(button_id);
|
||||
if (elem.tagName === "BUTTON") {
|
||||
if (elem.classList.contains("toggleOn")) { //toggle the pressed button off
|
||||
elem.classList.remove("toggleOn");
|
||||
} else {
|
||||
elem.classList.add("toggleOn");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the input object is undefined, make it "match" the target type
|
||||
* with default value (0 or empty str).
|
||||
*/
|
||||
function matchType(object, target) {
|
||||
if (typeof object === 'undefined') {
|
||||
switch (target) {
|
||||
case 'string':
|
||||
return "";
|
||||
case 'number':
|
||||
return 0;
|
||||
case 'undefined':
|
||||
return undefined;
|
||||
default:
|
||||
throw new Error(`Incomparable type ${target}`);
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
/** A utility function that reloads the page forcefully.
|
||||
*
|
||||
*/
|
||||
async function hardReload() {
|
||||
//https://gist.github.com/rmehner/b9a41d9f659c9b1c3340
|
||||
try {
|
||||
const dbs = await window.indexedDB.databases();
|
||||
await dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) });
|
||||
} catch (error) {
|
||||
// Hacky patch for firefox...
|
||||
console.log(error);
|
||||
const db_names = ['item_db', 'ing_db', 'map_db', 'tome_db'];
|
||||
await db_names.forEach(db => { window.indexedDB.deleteDatabase(db) });
|
||||
}
|
||||
|
||||
location.reload(true);
|
||||
}
|
||||
|
||||
|
||||
function capitalizeFirst(str) {
|
||||
return str[0].toUpperCase() + str.substring(1);
|
||||
}
|
||||
|
||||
/** https://stackoverflow.com/questions/16839698/jquery-getscript-alternative-in-native-javascript
|
||||
* If we ever want to write something that needs to import other js files
|
||||
*/
|
||||
const getScript = url => new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.async = true;
|
||||
|
||||
script.onerror = reject;
|
||||
|
||||
script.onload = script.onreadystatechange = function () {
|
||||
const loadState = this.readyState;
|
||||
|
||||
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
|
||||
|
||||
script.onload = script.onreadystatechange = null;
|
||||
|
||||
resolve();
|
||||
}
|
||||
|
||||
document.head.appendChild(script);
|
||||
})
|
||||
|
||||
/*
|
||||
GENERIC TEST FUNCTIONS
|
||||
*/
|
||||
/** The generic assert function. Fails on all "false-y" values. Useful for non-object equality checks, boolean value checks, and existence checks.
|
||||
*
|
||||
* @param {*} arg - argument to assert.
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert(arg, msg) {
|
||||
if (!arg) {
|
||||
throw new Error(msg ? msg : "Assert failed.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts object equality of the 2 parameters. For loose and strict asserts, use assert().
|
||||
*
|
||||
* @param {*} arg1 - first argument to compare.
|
||||
* @param {*} arg2 - second argument to compare.
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert_equals(arg1, arg2, msg) {
|
||||
if (!Object.is(arg1, arg2)) {
|
||||
throw new Error(msg ? msg : "Assert Equals failed. " + arg1 + " is not " + arg2 + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts object inequality of the 2 parameters. For loose and strict asserts, use assert().
|
||||
*
|
||||
* @param {*} arg1 - first argument to compare.
|
||||
* @param {*} arg2 - second argument to compare.
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert_not_equals(arg1, arg2, msg) {
|
||||
if (Object.is(arg1, arg2)) {
|
||||
throw new Error(msg ? msg : "Assert Not Equals failed. " + arg1 + " is " + arg2 + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts proximity between 2 arguments. Should be used for any floating point datatype.
|
||||
*
|
||||
* @param {*} arg1 - first argument to compare.
|
||||
* @param {*} arg2 - second argument to compare.
|
||||
* @param {Number} epsilon - the margin of error (<= del difference is ok). Defaults to -1E5.
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert_near(arg1, arg2, epsilon = 1E-5, msg) {
|
||||
if (Math.abs(arg1 - arg2) > epsilon) {
|
||||
throw new Error(msg ? msg : "Assert Near failed. " + arg1 + " is not within " + epsilon + " of " + arg2 + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that the input argument is null.
|
||||
*
|
||||
* @param {*} arg - the argument to test for null.
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert_null(arg, msg) {
|
||||
if (arg !== null) {
|
||||
throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not null.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that the input argument is undefined.
|
||||
*
|
||||
* @param {*} arg - the argument to test for undefined.
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert_undefined(arg, msg) {
|
||||
if (arg !== undefined) {
|
||||
throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not undefined.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that there is an error when a callback function is run.
|
||||
*
|
||||
* @param {Function} func_binding - a function binding to run. Can be passed in with func.bind(null, arg1, ..., argn)
|
||||
* @param {String} msg - the error message to throw.
|
||||
*/
|
||||
function assert_error(func_binding, msg) {
|
||||
try {
|
||||
func_binding();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
throw new Error(msg ? msg : "Function didn't throw an error.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep copy object/array of basic types.
|
||||
*/
|
||||
function deepcopy(obj, refs=undefined) {
|
||||
if (refs === undefined) {
|
||||
refs = new Map();
|
||||
}
|
||||
if (typeof(obj) !== 'object' || obj === null) { // null or value type
|
||||
return obj;
|
||||
}
|
||||
let ret = Array.isArray(obj) ? [] : {};
|
||||
for (let key in obj) {
|
||||
let val;
|
||||
try {
|
||||
val = obj[key];
|
||||
} catch (exc) {
|
||||
console.trace();
|
||||
val = undefined;
|
||||
}
|
||||
if (typeof(obj) === 'object') {
|
||||
if (refs.has(val)) {
|
||||
ret[key] = refs.get(val);
|
||||
}
|
||||
else {
|
||||
refs.set(val, val);
|
||||
ret[key] = deepcopy(val, refs);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret[key] = val;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function gen_slider_labeled({label_name, label_classlist = [], min = 0, max = 100, step = 1, default_val = min, id = undefined, color = "#FFFFFF", classlist = []}) {
|
||||
let slider_container = document.createElement("div");
|
||||
slider_container.classList.add("col");
|
||||
|
||||
let buf_col = document.createElement("div");
|
||||
|
||||
let label = document.createElement("div");
|
||||
label.classList.add(...label_classlist);
|
||||
label.textContent = label_name + ": " + default_val;
|
||||
|
||||
let slider = gen_slider(min, max, step, default_val, id, color, classlist, label);
|
||||
|
||||
//we set IDs here because the slider's id is potentially only meaningful after gen_slider() is called
|
||||
label.id = slider.id + "_label";
|
||||
slider_container.id = slider.id + "-container";
|
||||
|
||||
buf_col.append(slider, label);
|
||||
slider_container.appendChild(buf_col);
|
||||
|
||||
return slider_container;
|
||||
}
|
||||
|
||||
/** Creates a slider input (input type = range) given styling parameters
|
||||
*
|
||||
* @param {Number | String} min - The minimum value for the slider. defaults to 0
|
||||
* @param {Number | String} max - The maximum value for the slider. defaults to 100
|
||||
* @param {Number | String} step - The granularity between possible values. defaults to 1
|
||||
* @param {Number | String} default_val - The default value to set the slider to.
|
||||
* @param {String} id - The element ID to use for the slider. defaults to the current date time
|
||||
* @param {String} color - The hex color to use for the slider. Needs the # character.
|
||||
* @param {Array<String>} classlist - A list of classes to add to the slider.
|
||||
* @returns
|
||||
*/
|
||||
function gen_slider(min = 0, max = 100, step = 1, default_val = min, id = undefined, color = "#FFFFFF", classlist = [], label = undefined) {
|
||||
//simple attribute vals
|
||||
let slider = document.createElement("input");
|
||||
slider.type = "range";
|
||||
slider.min = min;
|
||||
slider.max = max;
|
||||
slider.step = step;
|
||||
slider.value = default_val;
|
||||
slider.autocomplete = "off";
|
||||
if (id) {
|
||||
if (document.getElementById(id)) {
|
||||
throw new Error("ID " + id + " already exists within the DOM.")
|
||||
} else {
|
||||
slider.id = id;
|
||||
}
|
||||
} else {
|
||||
slider.id = new Date().toLocaleTimeString();
|
||||
}
|
||||
slider.color = color;
|
||||
slider.classList.add(...classlist); //special spread operator -
|
||||
//necessary for display purposes
|
||||
slider.style.webkitAppearance = "none";
|
||||
slider.style.borderRadius = "30px";
|
||||
slider.style.height = "0.5rem";
|
||||
slider.classList.add("px-0", "slider");
|
||||
|
||||
//set up recoloring
|
||||
slider.addEventListener("change", function(e) {
|
||||
recolor_slider(slider, label);
|
||||
});
|
||||
//do recoloring for the default val
|
||||
let pct = Math.round(100 * (parseInt(slider.value) - parseInt(slider.min)) / (parseInt(slider.max) - parseInt(slider.min)));
|
||||
slider.style.background = `rgba(0, 0, 0, 0) linear-gradient(to right, ${color}, ${color} ${pct}%, #AAAAAA ${pct}%, #AAAAAA 100%)`;
|
||||
|
||||
//return slider
|
||||
return slider;
|
||||
}
|
||||
|
||||
/** Recolors a slider. If the corresponding label exists, also update that.
|
||||
*
|
||||
* @param {slider} slider - the slider element
|
||||
* @param {label} label - the label element
|
||||
*/
|
||||
function recolor_slider(slider, label) {
|
||||
let color = slider.color;
|
||||
let pct = Math.round(100 * (parseInt(slider.value) - parseInt(slider.min)) / (parseInt(slider.max) - parseInt(slider.min)));
|
||||
slider.style.background = `rgba(0, 0, 0, 0) linear-gradient(to right, ${color}, ${color} ${pct}%, #AAAAAA ${pct}%, #AAAAAA 100%)`;
|
||||
|
||||
if (label) {
|
||||
//convention is that the number goes at the end... I parse by separating it at ':'
|
||||
label.textContent = label.textContent.split(":")[0] + ": " + slider.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for making an element in html.
|
||||
*
|
||||
* @param {String} type : type of element
|
||||
* @param {List[String]} classlist : css classes for element
|
||||
* @param {Map[String, String]} args : Properties for the element
|
||||
*/
|
||||
function make_elem(type, classlist = [], args = {}) {
|
||||
const ret_elem = document.createElement(type);
|
||||
ret_elem.classList.add(...classlist);
|
||||
for (const i in args) {
|
||||
if (i === 'style') {
|
||||
const style_obj = args[i];
|
||||
if (typeof style_obj === 'string' || style_obj instanceof String) {
|
||||
ret_elem.style = style_obj;
|
||||
continue;
|
||||
}
|
||||
for (const k in style_obj) {
|
||||
ret_elem.style[k] = style_obj[k];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ret_elem[i] = args[i];
|
||||
}
|
||||
return ret_elem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nodes must have:
|
||||
* node: {
|
||||
* parents: List[node]
|
||||
* children: List[node]
|
||||
* }
|
||||
*
|
||||
* This function will define: "visited, assigned, scc" properties
|
||||
* Assuming a connected graph. (only one root)
|
||||
*/
|
||||
function make_SCC_graph(root_node, nodes) {
|
||||
for (const node of nodes) {
|
||||
node.visited = false;
|
||||
node.assigned = false;
|
||||
node.scc = null;
|
||||
}
|
||||
const res = []
|
||||
/*
|
||||
* SCC graph construction.
|
||||
* https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm
|
||||
*/
|
||||
function visit(u, res) {
|
||||
if (u.visited) { return; }
|
||||
u.visited = true;
|
||||
for (const child of u.children) {
|
||||
if (!child.visited) { visit(child, res); }
|
||||
}
|
||||
res.push(u);
|
||||
}
|
||||
visit(root_node, res);
|
||||
res.reverse();
|
||||
const sccs = [];
|
||||
function assign(node, cur_scc) {
|
||||
if (node.assigned) { return; }
|
||||
cur_scc.nodes.push(node);
|
||||
node.scc = cur_scc;
|
||||
node.assigned = true;
|
||||
for (const parent of node.parents) {
|
||||
assign(parent, cur_scc);
|
||||
}
|
||||
}
|
||||
for (const node of res) {
|
||||
if (node.assigned) { continue; }
|
||||
const cur_scc = {
|
||||
nodes: [],
|
||||
children: new Set(),
|
||||
parents: new Set()
|
||||
};
|
||||
assign(node, cur_scc);
|
||||
sccs.push(cur_scc);
|
||||
}
|
||||
for (const scc of sccs) {
|
||||
for (const node of scc.nodes) {
|
||||
for (const child of node.children) {
|
||||
scc.children.add(child.scc);
|
||||
}
|
||||
for (const parent of node.parents) {
|
||||
scc.parents.add(parent.scc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sccs;
|
||||
}
|
||||
|
||||
|
||||
// Toggles display of a certain element, given the ID.
|
||||
function toggle_tab(tab) {
|
||||
let elem = document.getElementById(tab);
|
||||
if (elem.style.display == "none") {
|
||||
elem.style.display = "";
|
||||
} else {
|
||||
elem.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle display of a certain tab, in a group of tabs, given the target tab ID, and a list of associated tabs.
|
||||
// Also sets visual display of an element with ID of target + "-btn" to selected.
|
||||
function show_tab(target, tabs) {
|
||||
//hide all tabs, then show the tab of the div clicked and highlight the correct button
|
||||
for (const i in tabs) {
|
||||
document.getElementById(tabs[i]).style.display = "none";
|
||||
document.getElementById(tabs[i] + "-btn").classList.remove("selected-btn");
|
||||
}
|
||||
document.getElementById(target).style.display = "";
|
||||
document.getElementById(target + "-btn").classList.add("selected-btn");
|
||||
}
|
||||
|
||||
// mobile navbar appearance control
|
||||
let scrollPos = 0
|
||||
if (screen.width < 992) {
|
||||
document.addEventListener('scroll', (e) => {
|
||||
if (document.documentElement.scrollTop - scrollPos > 20) {
|
||||
document.getElementById("mobile-navbar").style.display = "none";
|
||||
document.getElementById("mobile-navbar-dropdown").style.display = "none";
|
||||
} else if (document.documentElement.scrollTop - scrollPos < -50 || scrollPos < 70) {
|
||||
document.getElementById("mobile-navbar").style.display = "";
|
||||
}
|
||||
scrollPos = document.documentElement.scrollTop;
|
||||
});
|
||||
}
|
1019
js/c++/utils.js.out
Normal file
1019
js/c++/utils.js.out
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue