diff --git a/bench/LootTable.bench.ts b/bench/LootTable.bench.ts new file mode 100644 index 000000000..4c63b90a5 --- /dev/null +++ b/bench/LootTable.bench.ts @@ -0,0 +1,46 @@ +import { bench, describe } from "vitest"; +import { LootTable } from "../src"; + +const New = new LootTable() + .add("Spirit shield", 1, 8) + .add("Holy elixir", 1, 3) + .oneIn(585, new LootTable().add("Spectral sigil", 1, 3).add("Arcane sigil", 1, 3).add("Elysian sigil", 1, 1)) + .add("Mystic robe top", 1, 18) + .add("Mystic robe bottom", 1, 18) + .add("Mystic air staff", 1, 12) + .add("Mystic water staff", 1, 12) + .add("Mystic earth staff", 1, 12) + .add("Mystic fire staff", 1, 12) + .add("Soul rune", 250, 32) + .add("Runite bolts", 250, 24) + .add("Death rune", 300, 22) + .add("Onyx bolts (e)", 175, 20) + .add("Cannonball", 2000, 17) + .add("Adamant arrow", 750, 17) + .add("Law rune", 250, 17) + .add("Cosmic rune", 500, 17) + .add("Raw shark", 70, 21) + .add("Pure essence", 2500, 21) + .add("Adamantite bar", 35, 18) + .add("Green dragonhide", 100, 18) + .add("Adamantite ore", 125, 17) + .add("Runite ore", 20, 12) + .add("Teak plank", 100, 12) + .add("Mahogany logs", 150, 12) + .add("Magic logs", 75, 12) + .add("Tuna potato", 30, 20) + .add("White berries", 120, 17) + .add("Desert goat horn", 120, 17) + .add("Watermelon seed", 24, 15) + .add("Coins", [20_000, 50_000], 12) + .add("Antidote++(4)", 40, 10) + .add("Ranarr seed", 10, 5) + .tertiary(200, "Clue scroll (elite)") + .tertiary(1000, "Jar of spirits") + .tertiary(5000, "Pet dark core"); + +describe("LootTable Implementations", () => { + bench("1", () => { + New.roll(); + }); +}); diff --git a/scripts/enum.ts b/scripts/enum.ts index 85fae1a26..db810028b 100644 --- a/scripts/enum.ts +++ b/scripts/enum.ts @@ -4,6 +4,14 @@ import { Items, Monsters } from "../src"; import { USELESS_ITEMS } from "../src/structures/Items"; import { moidLink } from "./prepareItems"; +export function safeItemName(itemName: string) { + let key = itemName; + key = key.replace("3rd", "third"); + key = key.replace(/[^\w\s]|_/g, ""); + key = key.replace(/\s+/g, "_"); + key = key.toUpperCase(); + return key; +} const exitingKeys = new Set(); const duplicates = new Set(); let str = "export enum EItem {"; @@ -32,11 +40,8 @@ outer: for (const item of Items.values()) { if (USELESS_ITEMS.includes(item.id)) { continue; } - let key = item.wiki_name ?? item.name; - key = key.replace("3rd", "third"); - key = key.replace(/[^\w\s]|_/g, ""); - key = key.replace(/\s+/g, "_"); - key = key.toUpperCase(); + const key = safeItemName(item.wiki_name ?? item.name); + if (exitingKeys.has(key)) { duplicates.add(item.id); continue; diff --git a/src/meta/types.ts b/src/meta/types.ts index e9ace2773..a95b0444b 100644 --- a/src/meta/types.ts +++ b/src/meta/types.ts @@ -275,6 +275,9 @@ export interface WikiPage { }[]; } +export interface IntKeyBank { + [key: number]: number; +} export interface ItemBank { [key: string]: number; } diff --git a/src/structures/Bank.ts b/src/structures/Bank.ts index e04a53c6e..2bb2fa5c5 100644 --- a/src/structures/Bank.ts +++ b/src/structures/Bank.ts @@ -1,55 +1,74 @@ import { randArrItem } from "e"; -import type { BankItem, Item, ItemBank, ReturnedLootItem } from "../meta/types"; -import { fasterResolveBank, resolveNameBank } from "../util/bank"; +import type { BankItem, IntKeyBank, Item, ItemBank, ReturnedLootItem } from "../meta/types"; import itemID from "../util/itemID"; import Items from "./Items"; const frozenErrorStr = "Tried to mutate a frozen Bank."; +const isValidInteger = (str: string): boolean => /^-?\d+$/.test(str); + +function makeFromInitialBankX(initialBank?: IntKeyBank | ItemBank | Bank) { + if (!initialBank) return new Map(); + if (initialBank instanceof Bank) { + return new Map(initialBank.map.entries()); + } + const entries = Object.entries(initialBank); + if (entries.length === 0) return new Map(); + if (isValidInteger(entries[0][0])) { + return new Map(entries.map(([k, v]) => [Number(k), v])); + } else { + return new Map(entries.map(([k, v]) => [Items.get(k)!.id, v])); + } +} + export default class Bank { - public bank: ItemBank = {}; + public map: Map; public frozen = false; - constructor(initialBank?: ItemBank | Bank) { - if (initialBank) { - this.bank = JSON.parse( - JSON.stringify(initialBank instanceof Bank ? initialBank.bank : fasterResolveBank(initialBank)), - ); - } + constructor(initialBank?: IntKeyBank | ItemBank | Bank) { + this.map = makeFromInitialBankX(initialBank); + } + + get bank() { + return Object.fromEntries(this.map); } public freeze(): this { this.frozen = true; - Object.freeze(this.bank); + Object.freeze(this.map); return this; } public amount(item: string | number): number { - return this.bank[typeof item === "string" ? itemID(item) : item] ?? 0; + const itemIDNum = typeof item === "string" ? itemID(item) : item; + return this.map.get(itemIDNum) ?? 0; } public addItem(item: number, quantity = 1): this { + if (this.frozen) throw new Error(frozenErrorStr); if (quantity < 1) return this; - if (this.bank[item]) this.bank[item] += quantity; - else this.bank[item] = quantity; + const current = this.map.get(item) ?? 0; + this.map.set(item, current + quantity); return this; } public removeItem(item: number | string, quantity = 1): this { - const currentValue = this.bank[item]; + if (this.frozen) throw new Error(frozenErrorStr); + const itemIDNum = typeof item === "string" ? itemID(item) : item; + const currentValue = this.map.get(itemIDNum); - if (typeof currentValue === "undefined") return this; + if (currentValue === undefined) return this; if (currentValue - quantity <= 0) { - delete this.bank[item]; + this.map.delete(itemIDNum); } else { - this.bank[item] = currentValue - quantity; + this.map.set(itemIDNum, currentValue - quantity); } return this; } - public add(item: string | number | ReturnedLootItem[] | ItemBank | Bank | Item | undefined, quantity = 1): Bank { + public add(item: string | number | ReturnedLootItem[] | IntKeyBank | Bank | Item | undefined, quantity = 1): Bank { if (this.frozen) throw new Error(frozenErrorStr); // Bank.add(123); @@ -64,7 +83,10 @@ export default class Bank { } if (item instanceof Bank) { - return this.add(item.bank); + for (const [itemID, qty] of item.map.entries()) { + this.addItem(itemID, qty); + } + return this; } if (!item) { @@ -81,17 +103,15 @@ export default class Bank { return this.addItem(_item.id, quantity); } - const firstKey: string | undefined = Object.keys(item)[0]; - if (firstKey === undefined) { - return this; - } - - if (Number.isNaN(Number(firstKey))) { - this.add(resolveNameBank(item)); - } else { - for (const [itemID, quantity] of Object.entries(item)) { - this.addItem(Number.parseInt(itemID), quantity); + for (const [itemID, qty] of Object.entries(item)) { + let int: number | undefined = Number.parseInt(itemID); + if (Number.isNaN(int)) { + int = Items.get(itemID)?.id; } + if (!int) { + throw new Error(`${itemID} is not a valid name or id`); + } + this.addItem(int, qty); } return this; @@ -112,44 +132,34 @@ export default class Bank { } if (item instanceof Bank) { - for (const [key, value] of Object.entries(item.bank)) { - this.removeItem(key, value); + for (const [itemID, qty] of item.map.entries()) { + this.removeItem(itemID, qty); if (this.length === 0) break; } return this; } - const firstKey = Object.keys(item)[0]; - if (firstKey === undefined) { - return this; - } - if (Array.isArray(item)) { for (const _item of item) this.remove(_item.item, _item.quantity); return this; } - if (Number.isNaN(Number(firstKey))) { - this.remove(resolveNameBank(item)); - } else { - return this.remove(new Bank(item)); - } - + this.remove(new Bank(item)); return this; } public random(): BankItem | null { - const entries = Object.entries(this.bank); + const entries = Array.from(this.map.entries()); if (entries.length === 0) return null; const randomEntry = randArrItem(entries); - return { id: Number(randomEntry[0]), qty: randomEntry[1] }; + return { id: randomEntry[0], qty: randomEntry[1] }; } public multiply(multiplier: number, itemsToNotMultiply?: number[]): this { if (this.frozen) throw new Error(frozenErrorStr); - for (const itemID of Object.keys(this.bank).map(Number)) { + for (const [itemID, quantity] of this.map.entries()) { if (itemsToNotMultiply?.includes(itemID)) continue; - this.bank[itemID] *= multiplier; + this.map.set(itemID, quantity * multiplier); } return this; } @@ -176,8 +186,8 @@ export default class Bank { public items(): [Item, number][] { const arr: [Item, number][] = []; - for (const [key, val] of Object.entries(this.bank)) { - arr.push([Items.get(Number.parseInt(key))!, val]); + for (const [key, val] of this.map.entries()) { + arr.push([Items.get(key)!, val]); } return arr; } @@ -189,7 +199,7 @@ export default class Bank { } public clone(): Bank { - return new Bank({ ...this.bank }); + return new Bank(this); } public fits(bank: Bank): number { @@ -198,36 +208,31 @@ export default class Bank { return divisions[0] ?? 0; } - public filter(fn: (item: Item, quantity: number) => boolean, mutate = false): Bank { + public filter(fn: (item: Item, quantity: number) => boolean): Bank { const result = new Bank(); for (const item of this.items()) { if (fn(...item)) { result.add(item[0].id, item[1]); } } - if (mutate) { - if (this.frozen) throw new Error(frozenErrorStr); - this.bank = result.bank; - return this; - } return result; } public toString(): string { - const entries = Object.entries(this.bank); + const entries = Array.from(this.map.entries()); if (entries.length === 0) { return "No items"; } const res = []; for (const [id, qty] of entries.sort((a, b) => b[1] - a[1])) { - res.push(`${qty.toLocaleString()}x ${Items.get(Number(id))?.name ?? "Unknown item"}`); + res.push(`${qty.toLocaleString()}x ${Items.get(id)?.name ?? "Unknown item"}`); } return res.join(", "); } public get length(): number { - return Object.keys(this.bank).length; + return this.map.size; } public value(): number { @@ -243,7 +248,7 @@ export default class Bank { for (const [item, quantity] of this.items()) { if (otherBank.amount(item.id) !== quantity) return false; } - return JSON.stringify(this.bank) === JSON.stringify(otherBank.bank); + return JSON.stringify([...this.map]) === JSON.stringify([...otherBank.map]); } public difference(otherBank: Bank): Bank { diff --git a/src/structures/LootTable.ts b/src/structures/LootTable.ts index 876e7e6f6..32418094f 100644 --- a/src/structures/LootTable.ts +++ b/src/structures/LootTable.ts @@ -1,10 +1,38 @@ -import { randFloat, randInt, reduceNumByPercent, roll } from "e"; - -import type { LootTableItem, LootTableMoreOptions, LootTableOptions, OneInItems } from "../meta/types"; +import type { LootTableOptions } from "../meta/types"; import itemID from "../util/itemID"; import Bank from "./Bank"; import Items from "./Items"; +export function reduceNumByPercent(value: number, percent: number): number { + if (percent <= 0) return value; + return value - value * (percent / 100); +} +export function randInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1) + min); +} +export function randFloat(min: number, max: number): number { + return Math.random() * (max - min) + min; +} + +export function roll(upperLimit: number): boolean { + return randInt(1, upperLimit) === 1; +} + +export interface LootTableMoreOptions { + multiply?: boolean; + freeze?: boolean; +} + +export interface LootTableItem { + item: number | LootTable | LootTableItem[]; + weight?: number; + quantity: number | number[]; + options?: LootTableMoreOptions; +} + +export interface OneInItems extends LootTableItem { + chance: number; +} export function isArrayOfItemTuples(x: readonly unknown[]): x is [string, (number | number[])?][] { return Array.isArray(x[0]); } @@ -178,9 +206,7 @@ export default class LootTable { return this; } - public roll(quantity = 1, options?: LootTableRollOptions): Bank { - const loot = new Bank(); - + public roll(quantity = 1, options?: LootTableRollOptions, loot = new Bank()): Bank { const effectiveTertiaryItems = options?.tertiaryItemPercentageChanges ? this.tertiaryItems.map(i => { if (typeof i.item !== "number") return i; diff --git a/test/Bank.test.ts b/test/Bank.test.ts index 6b4d346cf..4fba556ce 100644 --- a/test/Bank.test.ts +++ b/test/Bank.test.ts @@ -22,24 +22,30 @@ describe("Bank", () => { test("bank has all items", () => { expect.assertions(2); - const bankToHave = new Bank({ - "Fire rune": 1000, - "Air rune": 1, - "Chaos rune": 101_010, - }); + const bankToHave = new Bank( + resolveNameBank({ + "Fire rune": 1000, + "Air rune": 1, + "Chaos rune": 101_010, + }), + ); - const bankThatShouldntHave = new Bank({ - "Fire rune": 1000, - "Air rune": 1, - "Chaos rune": 1, - }); + const bankThatShouldntHave = new Bank( + resolveNameBank({ + "Fire rune": 1000, + "Air rune": 1, + "Chaos rune": 1, + }), + ); - const bankThatShouldHave = new Bank({ - "Fire rune": 104_200, - "Air rune": 43_432, - "Chaos rune": 121_010, - "Death rune": 121_010, - }); + const bankThatShouldHave = new Bank( + resolveNameBank({ + "Fire rune": 104_200, + "Air rune": 43_432, + "Chaos rune": 121_010, + "Death rune": 121_010, + }), + ); expect(bankThatShouldHave.has(bankToHave)).toBeTruthy(); expect(bankThatShouldntHave.has(bankToHave)).toBeFalsy(); @@ -69,19 +75,25 @@ describe("Bank", () => { test("remove bank from bank", () => { expect.assertions(1); - const sourceBank = new Bank({ - "Fire rune": 100, - "Air rune": 50, - }); + const sourceBank = new Bank( + resolveNameBank({ + "Fire rune": 100, + "Air rune": 50, + }), + ); - const bankToRemove = new Bank({ - "Fire rune": 50, - "Air rune": 50, - }); + const bankToRemove = new Bank( + resolveNameBank({ + "Fire rune": 50, + "Air rune": 50, + }), + ); - const expectedBank = new Bank({ - "Fire rune": 50, - }); + const expectedBank = new Bank( + resolveNameBank({ + "Fire rune": 50, + }), + ); sourceBank.remove(bankToRemove); expect(sourceBank.equals(expectedBank)).toBeTruthy(); @@ -147,38 +159,30 @@ describe("Bank", () => { expect(bank.amount("Egg")).toEqual(100); }); - test("mutate filter", () => { - const bank = new Bank({ - Toolkit: 2, - "Ammo Mould": 4, - Candle: 1, - }); - expect(bank.length).toEqual(3); - const empty = bank.filter(() => false); - expect(bank.length).toEqual(3); - expect(empty.length).toEqual(0); - bank.filter(item => item.name === "Candle", true); - expect(bank.length).toEqual(1); - }); - test("value", () => { - const bank = new Bank({ - Toolkit: 2, - }); + const bank = new Bank( + resolveNameBank({ + Toolkit: 2, + }), + ); expect(bank.value()).toEqual(0); const runePlatebody = Items.get("Rune platebody")!; - const bank2 = new Bank({ - "Rune platebody": 10, - }); + const bank2 = new Bank( + resolveNameBank({ + "Rune platebody": 10, + }), + ); expect(runePlatebody.price).toBeGreaterThan(25_000); expect(bank2.value()).toEqual(runePlatebody.price * 10); - const bank3 = new Bank({ - "Rune platebody": 10, - "Rune platelegs": 10, - "Rune boots": 10, - Toolkit: 1, - "Abyssal book": 10_000, - }); + const bank3 = new Bank( + resolveNameBank({ + "Rune platebody": 10, + "Rune platelegs": 10, + "Rune boots": 10, + Toolkit: 1, + "Abyssal book": 10_000, + }), + ); expect(runePlatebody.price).toBeGreaterThan(25_000); expect(bank3.value()).toEqual( runePlatebody.price * 10 + Items.get("Rune platelegs")!.price * 10 + Items.get("Rune boots")!.price * 10, @@ -194,7 +198,7 @@ describe("Bank", () => { start[2] = 1; bank.bank[2] = 1; bank = bank.multiply(100); - bank.bank = {}; + bank.map.clear(); expect(bankToTest.amount(1)).toEqual(1); expect(bankToTest.length).toEqual(1); }); @@ -219,9 +223,6 @@ describe("Bank", () => { try { bank.multiply(5); } catch {} - try { - bank.filter(() => true, true); - } catch {} expect(bank.amount("Twisted bow")).toEqual(73); }); diff --git a/test/BankClass.test.ts b/test/BankClass.test.ts index 5af3be133..08a671912 100644 --- a/test/BankClass.test.ts +++ b/test/BankClass.test.ts @@ -75,10 +75,10 @@ describe("Bank Class", () => { bank.remove({ 1: 4 }); expect(bank.amount(1)).toBe(0); - bank.add({ Toolkit: 4 }); + bank.add(resolveNameBank({ Toolkit: 4 })); expect(bank.amount(1)).toBe(4); - bank.remove({ Toolkit: 4 }); + bank.remove(resolveNameBank({ Toolkit: 4 })); expect(bank.amount(1)).toBe(0); bank.add(TestLootTable.roll()); @@ -281,8 +281,7 @@ describe("Bank Class", () => { const bank = new Bank().add("Twisted bow").freeze(); expect(() => bank.add("Coal")).toThrow(); - expect(() => (bank.bank[itemID("Coal")] = 1)).toThrow(); - expect(() => delete bank.bank[itemID("Twisted bow")]).toThrow(); + expect(() => bank.remove("Twisted bow")).toThrow(); expect(bank.bank).toEqual({ [itemID("Twisted bow")]: 1 }); }); diff --git a/vitest.config.mts b/vitest.config.mts index 572e2a43e..ef11a5287 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -3,6 +3,9 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { name: "OldschoolJS", + benchmark: { + include: ["bench/**/*.bench.ts"], + }, include: ["test/**/*.test.ts"], coverage: { provider: "v8",