lootalot
Version:
Library for simulating loot table drops of arbitrary trial counts
399 lines (392 loc) • 14 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _classCallCheck(a, n) {
if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
}
function _defineProperties(e, r) {
for (var t = 0; t < r.length; t++) {
var o = r[t];
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);
}
}
function _createClass(e, r, t) {
return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
writable: !1
}), e;
}
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
t && (r = t);
var n = 0,
F = function () {};
return {
s: F,
n: function () {
return n >= r.length ? {
done: !0
} : {
done: !1,
value: r[n++]
};
},
e: function (r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = !0,
u = !1;
return {
s: function () {
t = t.call(r);
},
n: function () {
var r = t.next();
return a = r.done, r;
},
e: function (r) {
u = !0, o = r;
},
f: function () {
try {
a || null == t.return || t.return();
} finally {
if (u) throw o;
}
}
};
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
/** The algorithm used to determine if two item entries are equal, used to merge loot results. */
var DuplicateSearchMode;
(function (DuplicateSearchMode) {
/** Items are compared using the loose equal operator (`==`). */
DuplicateSearchMode["equal"] = "equal";
/** Items are compared using the strict equal operator (`===`). */
DuplicateSearchMode["strict_equal"] = "strict_equal";
/** Items are converted to JSON strings and then compared using the strict equal operator (`===`). */
DuplicateSearchMode["json"] = "json";
})(DuplicateSearchMode || (DuplicateSearchMode = {}));
/** Preferences object. */
var prefs = {
/** The maximum allowed precision error of thresholds such as the sum of probabilty values error. */
ARITHMETIC_ERROR: 1e-8,
/** The maximum amount of times can we roll the RNG manually (for accuracy)
* before it's better to approxmiate the rolling using math (for performance) instead. */
MAX_REPEAT: 20,
/** The default value of new loot tables' preferences object */
DEFAULT_TABLE_PREFS: {
duplicateSearchMode: "equal"
}
};
/**
* Inverse error function
*/
function erf_inv(x) {
var a = 0.1400122886866665;
var b = Math.log(1 - Math.pow(x, 2)) / a;
var c = 2 / Math.PI / a + Math.log(1 - Math.pow(x, 2)) / 2;
return Math.sign(x) * Math.sqrt(Math.sqrt(Math.pow(c, 2) - b) - c);
}
/**
* Probit function, or normal distribution quantile function if `μ` and `σ` is set
*/
function probit(p) {
var μ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var σ = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
var sqrt2 = 1.4142135623730951;
return erf_inv(2 * p - 1) * sqrt2 * σ + μ;
}
/**
* Flip `n` weighted coins, with `p` chance of landing on heads, and returns the number of heads.
* @argument {number} p - Probabilty of landing on heads.
* @argument {number} n - Amount of coins to flip.
*/
function coin_flip(n, p) {
if (p <= 0) return 0;
if (p >= 1) return n;
if (n <= prefs.MAX_REPEAT) {
var successes = 0;
for (var i = 0; i < n; i++) successes += +(Math.random() < p);
return successes;
} else {
var μ = n * p;
var σ = Math.sqrt(μ * (1 - p));
return Math.round(clamp(probit(Math.random(), μ, σ), 0, n));
}
}
/**
* Roll `n` dice, with face values ranging from `min` to `max`, and returns the sum of the roll dice's face values.
* @argument {number} n - Amount of dice to roll.
* @argument {number} min - The minimum dice value. (inclusive)
* @argument {number} max - The maximum dice value. (inclusive)
* @argument {number} step - The distance between dice values.
*/
function dice_roll(n, min, max) {
var step = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
if (min == max) return min * n;
if (n <= prefs.MAX_REPEAT) {
var value = 0;
for (var i = 0; i < n; i++) {
var roll = Math.random() * (max - min + step) + min;
if (step > 0) roll = Math.floor(roll / step) * step;
value += roll;
}
return value;
} else {
var μ = (max + min) / 2 * n;
var σ = Math.sqrt((Math.pow(max - min, 2) - 1) / 12 * n);
if (step > 0) {
step = gcd(min, max, step);
return clamp(Math.round(probit(Math.random(), μ / step, σ / step)) * step, min * n, max * n);
} else {
return clamp(probit(Math.random(), μ, σ), min * n, max * n);
}
}
}
function gcd() {
for (var _len = arguments.length, values = new Array(_len), _key = 0; _key < _len; _key++) {
values[_key] = arguments[_key];
}
var a = values[0],
b = values[1];
if (values.length > 2) b = gcd.apply(void 0, _toConsumableArray(values.slice(1)));
if (a < b) {
var _ref = [b, a];
a = _ref[0];
b = _ref[1];
}
while (a % b != 0) {
var _ref2 = [b, a % b];
a = _ref2[0];
b = _ref2[1];
}
return b;
}
function clamp(x, min, max) {
return Math.max(Math.min(x, max), min);
}
/** A loot table, containing the rules used to determine loot drops. */
var LootTable = /*#__PURE__*/function () {
function LootTable() {
_classCallCheck(this, LootTable);
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
this.pools = [];
this.prefs = Object.assign({}, prefs.DEFAULT_TABLE_PREFS);
this.prefs = Object.assign({}, prefs.DEFAULT_TABLE_PREFS);
for (var _len = arguments.length, pools = new Array(_len), _key = 0; _key < _len; _key++) {
pools[_key] = arguments[_key];
}
for (var _i = 0, _pools = pools; _i < _pools.length; _i++) {
var pool = _pools[_i];
var newPool = [];
this.pools.push(newPool);
var wExists = false,
pExists = false,
pSum = 0,
wSum = 0;
var _iterator = _createForOfIteratorHelper(pool),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _def = _step.value;
wExists || (wExists = _def.w !== undefined || _def.p === undefined);
pExists || (pExists = _def.p !== undefined);
pSum += (_a = _def.p) !== null && _a !== void 0 ? _a : 0;
wSum += (_b = _def.w) !== null && _b !== void 0 ? _b : 1;
if (wExists && pExists) throw Error("All loot definitions in a pool must either use `p` for probability or `w` for weight");
if (((_c = _def.w) !== null && _c !== void 0 ? _c : 1) < 0) throw Error("Weight can not be negative");
if (((_d = _def.p) !== null && _d !== void 0 ? _d : 0) < 0) throw Error("Probabilty can not be negative");
if (((_e = _def.step) !== null && _e !== void 0 ? _e : 1) < 0) throw Error("Step can not be negative");
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
if (pSum > 1 + prefs.ARITHMETIC_ERROR) throw Error("All loot definitions in a pool must have their `p` values sum to 1 or less (sum = " + pSum + ")");
if (pExists) wSum = 1;
var _iterator2 = _createForOfIteratorHelper(pool),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _def2 = _step2.value;
var item = {
w: (_g = (_f = _def2.w) !== null && _f !== void 0 ? _f : _def2.p) !== null && _g !== void 0 ? _g : 1,
cascadeP: 0,
item: _def2.item,
table: _def2.table,
count: (_h = _def2.count) !== null && _h !== void 0 ? _h : 1,
step: (_j = _def2.step) !== null && _j !== void 0 ? _j : getPreferredStepCount(_def2.count)
};
// @ts-expect-error
newPool.push(item);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (pExists && pSum < 1 - prefs.ARITHMETIC_ERROR) {
newPool.push({
w: 1 - pSum,
cascadeP: 0,
count: 1,
step: 1
});
}
// Sort our item list by most common first
newPool.sort(function (x, y) {
return y.w - x.w;
});
// Calculate cascading probability
for (var _i2 = 0, _newPool = newPool; _i2 < _newPool.length; _i2++) {
var def = _newPool[_i2];
def.cascadeP = def.w / wSum;
wSum -= def.w;
}
newPool[newPool.length - 1].cascadeP = 1;
}
}
/** Loot this loot table.
* @argument {number} trials - The amount of times will this function loot
*/
return _createClass(LootTable, [{
key: "loot",
value: function loot(trials) {
var _this = this;
var result = [];
var dupFunc = function dupFunc(a, b) {
return _this.isDuplicate.call(_this, a, b);
};
function addItem(item, count) {
var entry = result.find(function (x) {
return dupFunc(x.item, item);
});
if (entry) entry.count += count;else result.push({
item: item,
count: count
});
}
var _iterator3 = _createForOfIteratorHelper(this.pools),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var pool = _step3.value;
var t = trials;
var _iterator4 = _createForOfIteratorHelper(pool),
_step4;
try {
var _loop = function _loop() {
var item = _step4.value;
var times = coin_flip(t, item.cascadeP);
if (times <= 0) return 0; // continue
t -= times;
var amount = 0;
if (typeof item.count == "number") amount = times * item.count;else amount = dice_roll(times, item.count[0], item.count[1], item.step);
if (item.table !== undefined) {
var childLoot = item.table.loot(amount);
if (item.item !== undefined) childLoot = childLoot.map(function (x) {
return Object.assign(Object.assign({}, x), {
item: Object.assign(Object.assign({}, item.item), x.item)
});
});
var _iterator5 = _createForOfIteratorHelper(childLoot),
_step5;
try {
for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
var _loot = _step5.value;
addItem(_loot.item, _loot.count);
}
} catch (err) {
_iterator5.e(err);
} finally {
_iterator5.f();
}
} else if (item.item !== undefined) {
addItem(item.item, amount);
}
if (t <= 0) return 1; // break
},
_ret;
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
_ret = _loop();
if (_ret === 0) continue;
if (_ret === 1) break;
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
return result;
}
}, {
key: "isDuplicate",
value: function isDuplicate(a, b) {
var pref = this.prefs.duplicateSearchMode;
switch (pref) {
case "equal":
return a == b;
case "strict_equal":
return a === b;
case "json":
return JSON.stringify(a) == JSON.stringify(b);
}
}
}]);
}();
function getPreferredStepCount(count) {
if (count === undefined) return 1;
if (typeof count == "number") return +(count % 1 == 0);else return +(count[0] % 1 == 0 && count[1] % 1 == 0);
}
exports.LootTable = LootTable;