diff --git a/README.md b/README.md index f0af97f..0a7b57f 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,13 @@ Naming a tab involves specifying what to do with all items in that tab by naming ### Actions * `mall` - * This will add the item to your mall store + * This will add the item to your mall store at the maximum possible price +* `low` + * This will add the item to your mall store at the 5th lowest price currently listed * `autosell` * This will autosell the item * `sell` - * This will either autosell the item or add it to your mall store. It will add it to your mall store only if there are less than 1000 stocked at autosell price + * This will either autosell the item or add it to your mall store at the maximum possible price. It will add it to your mall store only if there are less than 1000 stocked at autosell price * `use` * This will use the item * `display` @@ -59,6 +61,8 @@ All options are supported in all tabs, unless specified. They are white space se * `keepN` * Keeps `N` copies of the item after running +* `stockN` + * (only supported by `mall`, `display`, and `closet`). Ensures `N` copies of the item are stocked in the relevant locations, keeps the rest in your inventory * `N` @@ -155,7 +159,8 @@ Use `keeping-tabs debug help` to see a full list of available debug commands. ## TODO -* [ ] Add more mall options (add at fixed price, add at min price, limit the items for sale) +* [ ] Add more mall options (add at fixed price, limit the items for sale) +* [x] Add more mall options (add at min price) * [ ] Add confirmation for kmailing, optionally? * [x] Add option to keep certain number of items (using format of keepN) * [ ] Add `pull` to pull specific items from Hagnks diff --git a/src/actions.ts b/src/actions.ts index 8946947..4fb53fb 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -2,6 +2,8 @@ import { autosell, autosellPrice, cliExecute, + closetAmount, + displayAmount, isCoinmasterItem, Item, itemAmount, @@ -10,6 +12,7 @@ import { putCloset, putDisplay, putShop, + shopAmount, use, wellStocked, } from "kolmafia"; @@ -44,12 +47,26 @@ export const actions: { }; } = { mall: (options: Options) => { + if (options.stock) { + return { + action: (item: Item) => + putShop( + 0, + 0, + Math.min(Math.max(0, (options.stock ?? 0) - shopAmount(item)), amount(item, options)), + item + ), + }; + } return { action: (item: Item) => putShop(0, 0, amount(item, options), item) }; }, sell: (options: Options) => { return { action: (item: Item) => { - if (wellStocked(`${item}`, 1000, Math.max(100, autosellPrice(item) * 2))) { + if ( + wellStocked(`${item}`, 1000, Math.max(100, autosellPrice(item) * 2)) || + !item.tradeable + ) { autosell(amount(item, options), item); } else { putShop(0, 0, amount(item, options), item); @@ -57,7 +74,26 @@ export const actions: { }, }; }, + low: (options) => { + return { + action: (item) => { + putShop(mallPrice(item), 0, amount(item, options), item); + }, + }; + }, display: (options: Options) => { + if (options.stock) { + return { + action: (item: Item) => + putDisplay( + Math.min( + Math.max(0, (options.stock ?? 0) - displayAmount(item)), + amount(item, options) + ), + item + ), + }; + } return { action: (item: Item) => putDisplay(amount(item, options), item) }; }, use: (options: Options) => { @@ -82,6 +118,15 @@ export const actions: { }; }, closet: (options: Options) => { + if (options.stock) { + return { + action: (item: Item) => + putDisplay( + Math.min(Math.max(0, (options.stock ?? 0) - closetAmount(item)), amount(item, options)), + item + ), + }; + } return { action: (item: Item) => putCloset(amount(item, options), item), }; diff --git a/src/main.ts b/src/main.ts index cf38451..7ea6175 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,95 @@ import { set } from "libram"; const HIGHLIGHT = isDarkMode() ? "yellow" : "blue"; const DEFAULT_ACTIONS = "closet use coinmaster mall autosell display sell kmail fuel collection"; +function items(tabId: TabId, type: InventoryType): Item[] { + const tab = visitUrl(`${type}.php?which=f${tabId}`); + const regexp = /ic(\d+)/g; + const items: Item[] = []; + + let match; + while ((match = regexp.exec(tab)) !== null) { + const item = toItem(toInt(match[1])); + items.push(item); + } + return items; +} + +function notesText(): string { + const questLogNotesHtml = visitUrl("questlog.php?which=4"); + return questLogNotesHtml.substring( + questLogNotesHtml.indexOf(">", questLogNotesHtml.indexOf(" { + const questLogAliases: RegExpExecArray[] = notesText() + .split("\n") + .map((s) => /keeping-tabs: ?([A-Za-z0-9\- ]+)=(.*)/g.exec(s)) + .filter((r) => r !== null) as RegExpExecArray[]; + + const values: [string, string][] = questLogAliases.map((r) => [r[1], r[2]]); + return new Map(values); +} + +function tabCollections(): Map { + const questLogEntries: RegExpExecArray[] = notesText() + .split("\n") + .map((s) => /keeping-tabs-collection: ?'(.*)'=([0-9,]+)/g.exec(s)) + .filter((r) => r !== null) as RegExpExecArray[]; + + const values: [string, Item[]][] = questLogEntries.map((r) => [ + r[1], + r[2].split(",").map((i) => toItem(toInt(i))), + ]); + return new Map(values); +} + +function favoriteTabs(): Tab[] { + // visit the consumables tab to ensure that you get clickable links for + // all favorite tabs + const inventory = visitUrl(`inventory.php?which=1`); + const tabRegex = + /([A-Za-z0-9;&]+)(:[A-Za-z0-9;&\-#,<>=]+)?<\/a>/g; + const aliasRegex = /([A-Za-z0-9;&]+)(:[A-Za-z0-9;&\-#,<>=]+)?/g; + + const tabs: Tab[] = []; + const aliases = tabAliases(); + + let match; + let aliasMatch; + + while ((match = tabRegex.exec(inventory)) !== null) { + const title = match[2]; + const options = match[3]; + const alias = aliases.get(title); + const id = parseInt(match[1]); + + if (isTabTitle(title)) { + tabs.push({ + title, + id, + options: (options ?? ":").substring(1).split(","), + type: "inventory", + }); + } else if (alias && (aliasMatch = aliasRegex.exec(alias))) { + const aliasTitle = aliasMatch[1]; + const options = aliasMatch[2]; + if (isTabTitle(aliasTitle)) { + tabs.push({ + title: aliasTitle, + id: parseInt(match[1]), + options: (options ?? ":").substring(1).split(","), + type: "inventory", + alias: title, + }); + } + } + } + + return tabs; +} + function tabString(tab: Tab): string { const options = Options.parse(tab.options); const title = tab.alias ? `${tab.title} (alias ${tab.alias})` : tab.title; diff --git a/src/options.ts b/src/options.ts index f057c49..a5d934c 100644 --- a/src/options.ts +++ b/src/options.ts @@ -7,6 +7,7 @@ type OptionsParams = { export class Options { keep?: number; + stock?: number; target?: string; body?: string; priceUpperThreshold?: number; @@ -28,6 +29,11 @@ export class Options { options.keep = parseInt(keep[1]); continue; } + const stock = optionStr.match(/stock(\d+)/); + if (stock && stock[1]) { + options.stock = parseInt(stock[1]); + continue; + } const target = optionStr.match(/#(.*)/); if (target && target[1]) { options.target = target[1]; @@ -65,6 +71,9 @@ export class Options { if (this.keep) { optionsStr.push(`keep: ${this.keep}`); } + if (this.stock) { + optionsStr.push(`stock: ${this.stock}`); + } if (this.target) { optionsStr.push(`target: ${this.target}`); } diff --git a/src/types.ts b/src/types.ts index d2c13dd..60e4e96 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,17 +10,18 @@ export const ALL_TAB_TITLES = [ "closet", "fuel", "collection", + "low", "coinmaster", ] as const; -export type TabTitle = (typeof ALL_TAB_TITLES)[number]; +export type TabTitle = typeof ALL_TAB_TITLES[number]; export type TabId = number; export function isTabTitle(value: string): value is TabTitle { return ALL_TAB_TITLES.includes(value as TabTitle); } -const ALL_ACTION_OPTIONS = ["keep", "target"] as const; -export type ActionOption = (typeof ALL_ACTION_OPTIONS)[number]; +const ALL_ACTION_OPTIONS = ["keep", "stock", "target"] as const; +export type ActionOption = typeof ALL_ACTION_OPTIONS[number]; export function isActionOption(value: string): value is ActionOption { return ALL_ACTION_OPTIONS.includes(value as ActionOption);