├── .husky └── pre-commit ├── .yarnrc.yml ├── .gitattributes ├── kolmafia-polyfill.js ├── .editorconfig ├── src ├── actions │ ├── index.ts │ └── FreeRun.ts ├── challengePaths │ ├── 2014 │ │ └── HeavyRains.ts │ ├── 2016 │ │ └── NuclearAutumn.ts │ └── index.ts ├── utils.test.ts ├── console.ts ├── moonSign.ts ├── Copier.ts ├── resources │ ├── 2006 │ │ └── CommaChameleon.ts │ ├── 2007 │ │ └── CandyHearts.ts │ ├── 2008 │ │ ├── DivineFavors.ts │ │ └── Stickers.ts │ ├── 2009 │ │ ├── LoveSongs.ts │ │ ├── SpookyPutty.ts │ │ └── Bandersnatch.ts │ ├── 2010 │ │ ├── Brickos.ts │ │ └── LookingGlass.ts │ ├── 2011 │ │ ├── Gygaxian.ts │ │ ├── ObtuseAngel.ts │ │ └── StompingBoots.ts │ ├── 2012 │ │ ├── Resolutions.ts │ │ ├── RainDoh.ts │ │ └── ReagnimatedGnome.ts │ ├── 2013 │ │ ├── PulledTaffy.ts │ │ └── JungMan.ts │ ├── 2014 │ │ ├── WinterGarden.ts │ │ └── CrimboShrub.ts │ ├── 2015 │ │ ├── BarrelShrine.ts │ │ ├── MayoClinic.ts │ │ ├── DeckOfEveryCard.ts │ │ └── ChateauMantegna.ts │ ├── 2016 │ │ ├── Witchess.ts │ │ └── GingerBread.ts │ ├── 2017 │ │ ├── MummingTrunk.ts │ │ ├── Horsery.ts │ │ ├── TunnelOfLove.ts │ │ └── Robortender.ts │ ├── 2018 │ │ └── SongBoom.ts │ ├── 2019 │ │ ├── PocketProfessor.ts │ │ ├── CampAway.ts │ │ ├── Snapper.ts │ │ └── BeachComb.ts │ ├── 2020 │ │ ├── Cartography.ts │ │ └── RetroCape.ts │ ├── 2021 │ │ ├── CrystalBall.ts │ │ └── DaylightShavings.ts │ ├── 2022 │ │ ├── JuneCleaver.ts │ │ ├── DesignerSweatpants.ts │ │ ├── GreyGoose.ts │ │ ├── CombatLoversLocket.ts │ │ └── Stillsuit.ts │ ├── 2023 │ │ ├── CinchoDeMayo.ts │ │ ├── AugustScepter.ts │ │ └── BurningLeaves.ts │ ├── 2024 │ │ ├── TearawayPants.ts │ │ ├── BatWings.ts │ │ ├── EverfullDarts.ts │ │ └── ChestMimic.ts │ ├── 2025 │ │ ├── CyberRealm.ts │ │ ├── ToyCupidBow.ts │ │ ├── SkeletonOfCrimboPast.ts │ │ ├── CrepeParachute.ts │ │ ├── PeridotOfPeril.ts │ │ └── BloodCubicZirconia.ts │ ├── evergreen │ │ ├── Raffle.test.ts │ │ └── Raffle.ts │ ├── putty-likes.ts │ └── LibramSummon.ts ├── overlappingNames.ts ├── index.ts ├── counter.ts ├── logger.ts ├── Kmail.test.ts ├── diet │ └── knapsack.test.ts ├── url.ts ├── modifierTypes.ts ├── propertyTyping.ts └── since.ts ├── examples ├── resources.ts ├── lib.ts ├── README.md ├── consult.ts ├── item.ts ├── props.ts └── clan.ts ├── tools ├── tsconfig.json ├── parseItemSkillNames.ts └── parseModifiers.ts ├── .prettierignore ├── .gitignore ├── vitest.config.ts ├── babel.config.cjs ├── README.md ├── CONTRIBUTING.md ├── .github └── workflows │ ├── publish.yml │ ├── docs.yml │ ├── properties.yml │ └── main.yml ├── tsconfig.json ├── eslint.config.mjs ├── package.json └── setupTests.ts /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.4.1.cjs 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force all text line endings to lf to match prettier config 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /kolmafia-polyfill.js: -------------------------------------------------------------------------------- 1 | const kolmafia = require("kolmafia"); 2 | export let console = { log: kolmafia.print }; 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | max_line_length = 80 8 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ActionSource.js"; 2 | export * from "./Banish.js"; 3 | export * from "./FreeKill.js"; 4 | export * from "./FreeRun.js"; 5 | -------------------------------------------------------------------------------- /examples/resources.ts: -------------------------------------------------------------------------------- 1 | import { console } from "../src"; 2 | import { SourceTerminal } from "../src/resources"; 3 | 4 | console.log(SourceTerminal.canDigitize()); 5 | -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./"], 3 | "extends": "../node_modules/@tsconfig/node20/tsconfig.json", 4 | "compilerOptions": { 5 | "module": "Node16" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | KoLmafia/ 3 | docs/ 4 | .yarn 5 | **/__fixtures__/** 6 | 7 | # Autogenerated stuff, should not be formatted 8 | src/propertyTypes.ts 9 | src/modifierTypes.ts 10 | -------------------------------------------------------------------------------- /examples/lib.ts: -------------------------------------------------------------------------------- 1 | import { console, getSongCount, $monster, getMonsterLocations } from "../src"; 2 | 3 | console.log(getSongCount()); 4 | 5 | console.log(getMonsterLocations($monster`zobmie`)); 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This directory contains some examples of code written with libram. 2 | 3 | We don't have a good way to build them right now because we removed webpack. But only Gausie ever used them so it's fine. 4 | -------------------------------------------------------------------------------- /src/challengePaths/index.ts: -------------------------------------------------------------------------------- 1 | import * as HeavyRains from "./2014/HeavyRains.js"; 2 | import CommunityService from "./2015/CommunityService.js"; 3 | import * as NuclearAutumn from "./2016/NuclearAutumn.js"; 4 | export { CommunityService, NuclearAutumn, HeavyRains }; 5 | -------------------------------------------------------------------------------- /examples/consult.ts: -------------------------------------------------------------------------------- 1 | // This is an example consult script using the macro handler in src/combat.ts. 2 | 3 | import { Macro } from "../src"; 4 | 5 | /** 6 | * Main function for the consult script. 7 | */ 8 | export function main(): void { 9 | Macro.load().submit(); 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | examples/dist/ 5 | KoLmafia/scripts/libram.js 6 | .vscode/ 7 | src/**/*.js 8 | src/**/*.d.ts 9 | /docs/ 10 | 11 | .pnp.* 12 | .yarn/* 13 | !.yarn/patches 14 | !.yarn/plugins 15 | !.yarn/releases 16 | !.yarn/sdks 17 | !.yarn/versions 18 | -------------------------------------------------------------------------------- /examples/item.ts: -------------------------------------------------------------------------------- 1 | import { console, $items } from "../src"; 2 | 3 | console.log( 4 | $items`lemon, lime`.map((i) => `${i.name} is ${i.quality}`).join(", "), 5 | ); 6 | 7 | console.log( 8 | $items`hilarious comedy prop, Victor\, the Insult Comic Hellhound Puppet, observational glasses` 9 | .map((i) => i.name) 10 | .join(", "), 11 | ); 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import GithubActionsReporter from "vitest-github-actions-reporter"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | setupFiles: ["./setupTests.ts"], 7 | reporters: process.env.GITHUB_ACTIONS 8 | ? ["default", new GithubActionsReporter()] 9 | : "default", 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | exclude: [], 5 | presets: [ 6 | "@babel/preset-typescript", 7 | [ 8 | "@babel/preset-env", 9 | { 10 | targets: { rhino: "1.7.14" }, 11 | }, 12 | ], 13 | ], 14 | plugins: ["@babel/plugin-proposal-object-rest-spread"], 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | 3 | import { splitByCommasWithEscapes } from "./utils.js"; 4 | 5 | describe("splitByCommasWithEscapes", () => { 6 | it("can split by commas with escapes", () => { 7 | const values = splitByCommasWithEscapes("weapon, off-hand, acc1"); 8 | 9 | expect(values).toEqual(["weapon", "off-hand", "acc1"]); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/console.ts: -------------------------------------------------------------------------------- 1 | import { print } from "kolmafia"; 2 | 3 | const logColor = 4 | (color?: string) => 5 | (...args: { toString(): string }[]) => { 6 | const output = args.map((x) => x.toString()).join(" "); 7 | if (color) { 8 | print(output, color); 9 | } else { 10 | print(output); 11 | } 12 | }; 13 | 14 | export const log = logColor(); 15 | export const info = logColor("blue"); 16 | export const warn = logColor("red"); 17 | export const error = logColor("red"); 18 | -------------------------------------------------------------------------------- /examples/props.ts: -------------------------------------------------------------------------------- 1 | import { get, set } from "../src"; 2 | import { withProperty } from "../src/property"; 3 | 4 | const raindoh = get("_raindohCopiesMade"); 5 | const putty = get("spookyPuttyCopiesMade"); 6 | 7 | console.log(raindoh + putty); 8 | 9 | console.log(`before: ${get("libram_test_prop", 0)}`); 10 | set("libram_test_prop", 30000); 11 | console.log(`after: ${get("libram_test_prop") + 5}`); 12 | 13 | withProperty("guzzlrBronzeDeliveries", 69, () => { 14 | console.log("nice"); 15 | }); 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libram 2 | 3 | `libram` is a Typescript library that intends to provide comprehensive support for automating KoLmafia. It is installable via e.g. `yarn add libram` or `npm install libram`. 4 | 5 | ## In-mafia support 6 | 7 | `libram` can also be used directly from the KoLmafia graphical command line. Start by installing with `git checkout https://github.com/loathers/libram release`. Then set up an alias of `jsl`: `alias jsl => js { Object.assign(globalThis, require("libram")); }`. You can then run e.g. `jsl AsdonMartin.fillTo(100)` to fill your Asdon Martin, or use any other libram component. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Development Environment 4 | 5 | Libram is primarily developed on Linux. To develop libram on Windows, we recommend using WSL. 6 | 7 | libram uses Yarn classic. We intend to eventually move to Yarn v2, but there are no solid plans yet. 8 | 9 | You can use any editor for development. If you are uncertain, we recommend using Visual Studio Code. 10 | 11 | ## Contributing Guidelines 12 | 13 | Before you submit a PR, please make sure to... 14 | 15 | - Run `yarn run format` to prettify your code 16 | - Run `yarn run lint` to check for gotchas in your code 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to NPM 2 | permissions: 3 | id-token: write # Required for OIDC 4 | contents: read 5 | on: 6 | release: 7 | types: [created] 8 | workflow_dispatch: {} 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: corepack enable 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | registry-url: "https://registry.npmjs.org" 19 | - run: yarn install --immutable 20 | - run: npm install -g npm@latest 21 | - run: npm publish 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "declaration": true, 5 | "lib": ["esnext"], 6 | "module": "ES2022", 7 | "moduleResolution": "bundler", 8 | "newLine": "LF", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "outDir": "dist", 13 | "skipLibCheck": true, 14 | "stripInternal": true, 15 | "resolveJsonModule": true, 16 | "exactOptionalPropertyTypes": true, 17 | "types": ["node"] 18 | }, 19 | "include": ["src/", "setupTests.ts"], 20 | "typedocOptions": { 21 | "entryPoints": ["src/index.ts"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - jsdoc 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out 16 | uses: actions/checkout@v4 17 | 18 | - run: corepack enable 19 | 20 | - name: Generate static docs 21 | run: | 22 | yarn install --immutable 23 | yarn run docs 24 | 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./docs 30 | -------------------------------------------------------------------------------- /src/moonSign.ts: -------------------------------------------------------------------------------- 1 | const MoonSigns = [ 2 | "Mongoose", 3 | "Wallaby", 4 | "Vole", 5 | "Platypus", 6 | "Opossum", 7 | "Marmot", 8 | "Wombat", 9 | "Blender", 10 | "Packrat", 11 | ] as const; 12 | export type MoonSign = (typeof MoonSigns)[number]; 13 | 14 | /** 15 | * @param moon Moon sign name 16 | * @returns Moon sign id else 0 17 | */ 18 | export function signNameToId(moon: MoonSign): number { 19 | return MoonSigns.indexOf(moon) + 1; 20 | } 21 | 22 | /** 23 | * @param id Moon sign id 24 | * @returns Name of moon sign else "None" 25 | */ 26 | export function signIdToName(id: number): MoonSign | "None" { 27 | return MoonSigns[id - 1] || "None"; 28 | } 29 | -------------------------------------------------------------------------------- /src/Copier.ts: -------------------------------------------------------------------------------- 1 | import { Monster } from "kolmafia"; 2 | 3 | export class Copier { 4 | readonly couldCopy: () => boolean; 5 | readonly prepare: (() => boolean) | null; 6 | readonly canCopy: () => boolean; 7 | readonly copiedMonster: () => Monster | null; 8 | readonly fightCopy: (() => boolean) | null = null; 9 | 10 | constructor( 11 | couldCopy: () => boolean, 12 | prepare: (() => boolean) | null, 13 | canCopy: () => boolean, 14 | copiedMonster: () => Monster | null, 15 | fightCopy?: () => boolean, 16 | ) { 17 | this.couldCopy = couldCopy; 18 | this.prepare = prepare; 19 | this.canCopy = canCopy; 20 | this.copiedMonster = copiedMonster; 21 | if (fightCopy) this.fightCopy = fightCopy; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/clan.ts: -------------------------------------------------------------------------------- 1 | import { getClanName, itemAmount, printHtml } from "kolmafia"; 2 | import { $item, Clan, console } from "../src/index"; 3 | 4 | const piglets = Clan.join("The Piglets of Fate"); 5 | 6 | Clan.with("The Hogs of Exploitery", () => { 7 | try { 8 | console.log(piglets.getMeatInCoffer()); 9 | } catch (e) { 10 | console.log(e.message); 11 | } 12 | }); 13 | 14 | printHtml( 15 | `1. In ${getClanName()} with my ${itemAmount($item`toast`)} toast`, 16 | ); 17 | 18 | Clan.withStash("The Hogs of Exploitery", [$item`toast`], () => { 19 | printHtml( 20 | `2. In ${getClanName()} with my ${itemAmount($item`toast`)} toast`, 21 | ); 22 | }); 23 | 24 | printHtml( 25 | `3. In ${getClanName()} with my ${itemAmount($item`toast`)} toast`, 26 | ); 27 | -------------------------------------------------------------------------------- /src/resources/2010/Brickos.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $skill } from "../../template-string.js"; 5 | 6 | const summonSkill = $skill`Summon BRICKOs`; 7 | 8 | /** 9 | * @returns true if the player can Summon BRICKOs 10 | */ 11 | export function have(): boolean { 12 | return _have(summonSkill); 13 | } 14 | 15 | /** 16 | * @returns map containing the chance of an item to be summoned 17 | */ 18 | export function expected(): Map { 19 | const eyeSummons = get("_brickoEyeSummons"); 20 | const eyeChance = eyeSummons === 3 ? 0.0 : eyeSummons === 0 ? 0.5 : 1.0 / 3.0; 21 | return new Map([ 22 | [$item`BRICKO eye brick`, eyeChance], 23 | [$item`BRICKO brick`, 3.0 - eyeChance], 24 | ]); 25 | } 26 | -------------------------------------------------------------------------------- /src/resources/2011/Gygaxian.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { $item, $skill } from "../../template-string.js"; 4 | 5 | const summonSkill = $skill`Summon Dice`; 6 | const libramChance = 1.0 / 6; 7 | const libramExpected = new Map([ 8 | [$item`d4`, libramChance], 9 | [$item`d6`, libramChance], 10 | [$item`d8`, libramChance], 11 | [$item`d10`, libramChance], 12 | [$item`d12`, libramChance], 13 | [$item`d20`, libramChance], 14 | ]); 15 | 16 | /** 17 | * @returns true if the player can Summon Dice 18 | */ 19 | export function have(): boolean { 20 | return _have(summonSkill); 21 | } 22 | 23 | /** 24 | * @returns map containing the chance of an item to be summoned 25 | */ 26 | export function expected(): Map { 27 | return libramExpected; 28 | } 29 | -------------------------------------------------------------------------------- /src/challengePaths/2016/NuclearAutumn.ts: -------------------------------------------------------------------------------- 1 | import { visitUrl } from "kolmafia"; 2 | import { $path } from "../../template-string.js"; 3 | 4 | /** 5 | * Visits the Cooling Tank on level 8 of the Fallout shelter to gain 300 rads 6 | */ 7 | export function coolingTank(): void { 8 | visitUrl("place.php?whichplace=falloutshelter&action=vault8"); 9 | } 10 | 11 | /** 12 | * Visits the Spa Simulation Chamber on level 4 of the Fallout shelter for 100 turns of "100% all stats" 13 | */ 14 | export function spa(): void { 15 | visitUrl("place.php?whichplace=falloutshelter&action=vault3"); 16 | } 17 | 18 | /** 19 | * Visits the Chronodynamics Laboratory on level 5 of the Fallout shelter to permanently increase radiation level by 3 20 | */ 21 | export function chronoLab(): void { 22 | visitUrl("place.php?whichplace=falloutshelter&action=vault5"); 23 | } 24 | 25 | export const path = $path`Nuclear Autumn`; 26 | -------------------------------------------------------------------------------- /src/resources/2007/CandyHearts.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { $item, $skill } from "../../template-string.js"; 4 | 5 | const summonSkill = $skill`Summon Candy Heart`; 6 | const libramChance = 1.0 / 6; 7 | const libramExpected = new Map([ 8 | [$item`green candy heart`, libramChance], 9 | [$item`lavender candy heart`, libramChance], 10 | [$item`orange candy heart`, libramChance], 11 | [$item`pink candy heart`, libramChance], 12 | [$item`white candy heart`, libramChance], 13 | [$item`yellow candy heart`, libramChance], 14 | ]); 15 | 16 | /** 17 | * @returns true if the player can Summon Candy Heart 18 | */ 19 | export function have(): boolean { 20 | return _have(summonSkill); 21 | } 22 | 23 | /** 24 | * @returns map containing the chance of an item to be summoned 25 | */ 26 | export function expected(): Map { 27 | return libramExpected; 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/properties.yml: -------------------------------------------------------------------------------- 1 | name: Update properties if necessary 2 | on: 3 | workflow_dispatch: {} 4 | schedule: 5 | - cron: 0 0 * * * 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - run: corepack enable 12 | 13 | - name: Install dependencies 14 | run: yarn install --immutable 15 | 16 | - name: Build the code 17 | run: yarn build 18 | 19 | - name: Update properties from mafia source 20 | run: yarn run updateProps 21 | 22 | - name: Create Pull Request 23 | uses: peter-evans/create-pull-request@v6 24 | with: 25 | commit-message: Update property types automatically 26 | title: Automated updates to property typings 27 | body: This is an automatically generated PR with updates to the typings of properties from KoLmafia that libram maintains. 28 | branch: automatic-property-type-updates 29 | -------------------------------------------------------------------------------- /src/overlappingNames.ts: -------------------------------------------------------------------------------- 1 | /** THIS FILE IS AUTOMATICALLY GENERATED. See tools/parseItemSkillNames.ts for more information */ 2 | 3 | export const overlappingItemNames = [ 4 | "spider web", 5 | "really sticky spider web", 6 | "dictionary", 7 | "NG", 8 | "Cloaca-Cola", 9 | "yo-yo", 10 | "top", 11 | "ball", 12 | "kite", 13 | "yo", 14 | "red potion", 15 | "blue potion", 16 | "bowling ball", 17 | "adder", 18 | "red button", 19 | "tennis ball", 20 | "pile of sand", 21 | "mushroom", 22 | "deluxe mushroom", 23 | "spoon", 24 | ]; 25 | 26 | export const overlappingSkillNames = [ 27 | "Lightning Bolt", 28 | "Shoot", 29 | "Thrust-Smack", 30 | "Headbutt", 31 | "Toss", 32 | "Knife in the Dark", 33 | "Sing", 34 | "Disarm", 35 | "LIGHT", 36 | "BURN", 37 | "Extract", 38 | "Meteor Shower", 39 | "Snipe", 40 | "Bite", 41 | "Kick", 42 | "Howl", 43 | "Cleave", 44 | "Boil", 45 | "Slice", 46 | "Rainbow", 47 | "Lightning Bolt", 48 | ]; 49 | -------------------------------------------------------------------------------- /src/resources/2009/LoveSongs.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { $item, $skill } from "../../template-string.js"; 4 | 5 | const summonSkill = $skill`Summon Love Song`; 6 | const libramChance = 1.0 / 6; 7 | const libramExpected = new Map([ 8 | [$item`love song of disturbing obsession`, libramChance], 9 | [$item`love song of icy revenge`, libramChance], 10 | [$item`love song of naughty innuendo`, libramChance], 11 | [$item`love song of smoldering passion`, libramChance], 12 | [$item`love song of sugary cuteness`, libramChance], 13 | [$item`love song of vague ambiguity`, libramChance], 14 | ]); 15 | 16 | /** 17 | * @returns true if the player can Summon Love Song 18 | */ 19 | export function have(): boolean { 20 | return _have(summonSkill); 21 | } 22 | 23 | /** 24 | * @returns map containing the chance of an item to be summoned 25 | */ 26 | export function expected(): Map { 27 | return libramExpected; 28 | } 29 | -------------------------------------------------------------------------------- /src/resources/2015/BarrelShrine.ts: -------------------------------------------------------------------------------- 1 | import { availableAmount, itemAmount, runChoice, visitUrl } from "kolmafia"; 2 | import { get } from "../../property.js"; 3 | import { $items } from "../../template-string.js"; 4 | import { sum } from "../../utils.js"; 5 | 6 | const BARRELS = $items`little firkin, normal barrel, big tun, weathered barrel, dusty barrel, disintegrating barrel, moist barrel, rotting barrel, mouldering barrel, barnacled barrel`; 7 | 8 | /** 9 | * @returns Whether we have the barrel shrine 10 | */ 11 | export function have() { 12 | return get("barrelShrineUnlocked"); 13 | } 14 | 15 | /** 16 | * Smashes all barrels in our inventory 17 | */ 18 | export function smashParty() { 19 | if (!have()) return; 20 | 21 | const total = sum(BARRELS, availableAmount); 22 | const owned = BARRELS.find((b) => itemAmount(b)); 23 | if (total <= 0 || !owned) return; 24 | 25 | visitUrl(`inv_use.php?pwd&whichitem=${owned.id}&choice=1`); 26 | 27 | for (let i = 0; i < total / 100; i++) { 28 | runChoice(2); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/resources/2008/DivineFavors.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $skill } from "../../template-string.js"; 5 | 6 | const summonSkill = $skill`Summon Party Favor`; 7 | 8 | /** 9 | * @returns true if the player can Summon Party Favors 10 | */ 11 | export function have(): boolean { 12 | return _have(summonSkill); 13 | } 14 | 15 | /** 16 | * @returns map containing the chance of an item to be summoned 17 | */ 18 | export function expected(): Map { 19 | const rareSummons = get("_favorRareSummons"); 20 | const totalRareChance = 1.0 / 2 ** (rareSummons + 1); 21 | const commonChance = (1.0 - totalRareChance) / 3; 22 | const rareChance = totalRareChance / 3; 23 | 24 | return new Map([ 25 | [$item`divine blowout`, commonChance], 26 | [$item`divine can of silly string`, commonChance], 27 | [$item`divine noisemaker`, commonChance], 28 | [$item`divine champagne flute`, rareChance], 29 | [$item`divine champagne popper`, rareChance], 30 | [$item`divine cracker`, rareChance], 31 | ]); 32 | } 33 | -------------------------------------------------------------------------------- /src/resources/2012/Resolutions.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { $item, $skill } from "../../template-string.js"; 4 | 5 | const summonSkill = $skill`Summon Resolutions`; 6 | const commonChance = 0.98 / 6; 7 | const rareChance = 0.02 / 3; 8 | const libramExpected = new Map([ 9 | [$item`resolution: be feistier`, commonChance], 10 | [$item`resolution: be happier`, commonChance], 11 | [$item`resolution: be sexier`, commonChance], 12 | [$item`resolution: be smarter`, commonChance], 13 | [$item`resolution: be stronger`, commonChance], 14 | [$item`resolution: be wealthier`, commonChance], 15 | [$item`resolution: be kinder`, rareChance], 16 | [$item`resolution: be luckier`, rareChance], 17 | [$item`resolution: be more adventurous`, rareChance], 18 | ]); 19 | 20 | /** 21 | * @returns Whether the player can Summon Resolutions 22 | */ 23 | export function have(): boolean { 24 | return _have(summonSkill); 25 | } 26 | 27 | /** 28 | * @returns Map containing the chance of an item to be summoned 29 | */ 30 | export function expected(): Map { 31 | return libramExpected; 32 | } 33 | -------------------------------------------------------------------------------- /src/resources/2017/MummingTrunk.ts: -------------------------------------------------------------------------------- 1 | import { Familiar, splitModifiers } from "kolmafia"; 2 | import { NumericModifier } from "../../modifierTypes.js"; 3 | import { get } from "../../property.js"; 4 | import { notNull } from "../../utils.js"; 5 | import { isNumericModifier } from "../../modifier.js"; 6 | 7 | const MUMMERY_MODS_PATTERN = /\[(\d*)\*fam\(([^)]*)\)/; 8 | /** 9 | * Parses the _mummeryMods preference into a Map for easier use. 10 | * 11 | * @returns A map, mapping Familiars to a Tuple consisting of the NumericModifier attached to the familiar, and the value thereof. 12 | */ 13 | export function currentCostumes(): Map< 14 | Familiar, 15 | readonly [NumericModifier, number] 16 | > { 17 | return new Map( 18 | Object.entries(splitModifiers(get("_mummeryMods"))) 19 | .map(([modifier, value]) => { 20 | if (!isNumericModifier(modifier)) return null; 21 | 22 | const matcher = value.match(MUMMERY_MODS_PATTERN); 23 | if (!matcher) return null; 24 | 25 | return [ 26 | Familiar.get(matcher[2]), 27 | [modifier, Number(matcher[1])], 28 | ] as const; 29 | }) 30 | .filter(notNull), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/resources/2012/RainDoh.ts: -------------------------------------------------------------------------------- 1 | import { Monster, use } from "kolmafia"; 2 | 3 | import { getFoldGroup, have as haveItem } from "../../lib.js"; 4 | import { get } from "../../property.js"; 5 | import { $item } from "../../template-string.js"; 6 | 7 | const box = $item`Rain-Doh black box`; 8 | 9 | /** 10 | * See whether the player has a Rain-Doh item in some form 11 | * 12 | * @returns Whether the player has any Rain-Doh item 13 | */ 14 | export function have(): boolean { 15 | return getFoldGroup(box).some((item) => haveItem(item)); 16 | } 17 | 18 | /** 19 | * Get Rain-Doh black box copies made today 20 | * 21 | * @returns Number of Rain-Doh black box copies made 22 | */ 23 | export function getRainDohBlackBoxCopiesMade(): number { 24 | return Math.max(0, get("_raindohCopiesMade")); 25 | } 26 | 27 | /** 28 | * Get the current Rain-doh box monster 29 | * 30 | * @returns Current Rain-doh box monster 31 | */ 32 | export function getRainDohBlackBoxMonster(): Monster | null { 33 | return get("rainDohMonster"); 34 | } 35 | 36 | /** 37 | * Use the Rain-Doh box full of monster (i.e. fight the monster probably) 38 | * 39 | * @returns Success 40 | */ 41 | export function useRainDohBlackBox(): boolean { 42 | return use(box); 43 | } 44 | -------------------------------------------------------------------------------- /src/resources/2025/CyberRealm.ts: -------------------------------------------------------------------------------- 1 | import { gamedayToInt, Item } from "kolmafia"; 2 | import { realmAvailable } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item } from "../../template-string.js"; 5 | 6 | /** 7 | * @returns Whether or not you have Cyber Realm 8 | */ 9 | export function have(): boolean { 10 | return get("crAlways"); 11 | } 12 | 13 | /** 14 | * @returns Whether or Cyber Realm is available to adventure in 15 | */ 16 | export function available(): boolean { 17 | return realmAvailable("cyber"); 18 | } 19 | 20 | const ZONE_3_ITEMS_ARRAY = [ 21 | $item`dedigitizer schematic: virtual cybertattoo`, // Index 0 → Day 1 22 | $item`dedigitizer schematic: SLEEP(5) rom chip`, 23 | $item`dedigitizer schematic: insignificant bit`, 24 | $item`dedigitizer schematic: OVERCLOCK(10) rom chip`, 25 | $item`dedigitizer schematic: hashing vise`, 26 | $item`dedigitizer schematic: geofencing rapier`, 27 | $item`dedigitizer schematic: STATS+++ rom chip`, 28 | $item`dedigitizer schematic: geofencing shield`, 29 | ]; 30 | 31 | /** 32 | * @returns The Zone 3 Item we expect to find today, based on KOL Calendar 33 | */ 34 | export function zone3Rewards(): Item { 35 | return ZONE_3_ITEMS_ARRAY[gamedayToInt() % 8]; 36 | } 37 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import jsdoc from "eslint-plugin-jsdoc"; 5 | import tseslint from "typescript-eslint"; 6 | import prettier from "eslint-config-prettier"; 7 | import libram from "eslint-plugin-libram"; 8 | 9 | export default tseslint.config( 10 | eslint.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | jsdoc.configs["flat/recommended-error"], 13 | prettier, 14 | ...libram.configs.recommended, 15 | { 16 | rules: { 17 | "@typescript-eslint/no-unused-vars": [ 18 | "warn", 19 | { varsIgnorePattern: "^_" }, 20 | ], 21 | "jsdoc/require-jsdoc": [ 22 | "error", 23 | { 24 | publicOnly: true, 25 | }, 26 | ], 27 | "jsdoc/require-param-type": 0, 28 | "jsdoc/require-returns-type": 0, 29 | "jsdoc/require-property-type": 0, 30 | "jsdoc/tag-lines": 0, 31 | "jsdoc/no-defaults": 0, 32 | "jsdoc/check-tag-names": [ 33 | "error", 34 | { 35 | // TypeDoc defines some additional valid tags https://typedoc.org/guides/tags/ 36 | definedTags: ["category", "packageDocumentation"], 37 | }, 38 | ], 39 | }, 40 | }, 41 | { 42 | ignores: ["dist", "KoLmafia", "**/*.{js,cjs}"], 43 | }, 44 | ); 45 | -------------------------------------------------------------------------------- /src/resources/2013/PulledTaffy.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $skill } from "../../template-string.js"; 5 | 6 | const summonSkill = $skill`Summon Taffy`; 7 | 8 | /** 9 | * @returns true if the player can Summon Taffy 10 | */ 11 | export function have(): boolean { 12 | return _have(summonSkill); 13 | } 14 | 15 | /** 16 | * @returns map containing the chance of an item to be summoned 17 | */ 18 | export function expected(): Map { 19 | const rareSummons = get("_taffyRareSummons"); 20 | const yellowSummons = get("_taffyYellowSummons"); 21 | const onlyYellow = yellowSummons === 0 && rareSummons === 3; 22 | const totalRareChance = rareSummons < 4 ? 1.0 / 2 ** (rareSummons + 1) : 0.0; 23 | const commonChance = (1.0 - totalRareChance) / 4; 24 | const rareChance = onlyYellow 25 | ? 0.0 26 | : totalRareChance / (3 - get("_taffyYellowSummons")); 27 | const yellowChance = 28 | yellowSummons === 1 ? 0.0 : onlyYellow ? totalRareChance : rareChance; 29 | 30 | return new Map([ 31 | [$item`pulled blue taffy`, commonChance], 32 | [$item`pulled orange taffy`, commonChance], 33 | [$item`pulled violet taffy`, commonChance], 34 | [$item`pulled red taffy`, commonChance], 35 | [$item`pulled indigo taffy`, rareChance], 36 | [$item`pulled green taffy`, rareChance], 37 | [$item`pulled yellow taffy`, yellowChance], 38 | ]); 39 | } 40 | -------------------------------------------------------------------------------- /src/resources/2009/SpookyPutty.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, Monster, use } from "kolmafia"; 2 | 3 | import { getFoldGroup, have as haveItem } from "../../lib.js"; 4 | import { get } from "../../property.js"; 5 | import { $item } from "../../template-string.js"; 6 | 7 | const sheet = $item`Spooky Putty sheet`; 8 | 9 | /** 10 | * See whether the player has a Spooky Putty item in some form 11 | * 12 | * @returns Whether the player has any Spooky Putty item 13 | */ 14 | export function have(): boolean { 15 | return getFoldGroup(sheet).some((item) => haveItem(item)); 16 | } 17 | 18 | /** 19 | * Get spooky putty sheet copies made today 20 | * 21 | * @returns Number of spooky putty sheet copies made 22 | */ 23 | export function getSpookyPuttySheetCopiesMade(): number { 24 | return Math.max(0, get("spookyPuttyCopiesMade")); 25 | } 26 | 27 | /** 28 | * Prepares a spooky putty sheet for use 29 | * 30 | * @returns Success 31 | */ 32 | export function prepareSpookyPuttySheet(): boolean { 33 | if (!have()) return false; 34 | if (haveItem(sheet)) return true; 35 | 36 | return cliExecute("fold Spooky putty sheet"); 37 | } 38 | 39 | /** 40 | * Get the current puttied monster 41 | * 42 | * @returns Current puttied monster 43 | */ 44 | export function getSpookyPuttySheetMonster(): Monster | null { 45 | return get("spookyPuttyMonster"); 46 | } 47 | 48 | /** 49 | * Use the spooky putty sheet (i.e. fight the monster probably) 50 | * 51 | * @returns Success 52 | */ 53 | export function useSpookyPuttySheet(): boolean { 54 | return use(sheet); 55 | } 56 | -------------------------------------------------------------------------------- /src/resources/2017/Horsery.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, visitUrl } from "kolmafia"; 2 | import { Modifiers } from "../../modifier.js"; 3 | import { get } from "../../property.js"; 4 | import { NumericModifier } from "../../modifierTypes.js"; 5 | 6 | /** 7 | * @returns Whether or not the horsery is available 8 | */ 9 | export function have(): boolean { 10 | return get("horseryAvailable"); 11 | } 12 | 13 | export type Horse = "pale" | "dark" | "normal" | "crazy"; 14 | 15 | /** 16 | * @returns Your current horse; `null` if you are horseless 17 | */ 18 | export function current(): Horse | null { 19 | const horse = get("_horsery"); 20 | return (horse ? horse.split(" ")[0] : null) as Horse | null; 21 | } 22 | 23 | /** 24 | * @param horse The horse to change to 25 | * @returns Whether, at the end of all things, that is your horse 26 | */ 27 | export function changeHorse(horse: Horse): boolean { 28 | if (horse === current()) return true; 29 | if (!have()) return false; 30 | cliExecute(`horsery ${horse}`); 31 | return current() === horse; 32 | } 33 | 34 | /** 35 | * @returns a `Modifiers` object consisting of the crazy horse's stats today 36 | */ 37 | export function crazyHorseStats(): Modifiers { 38 | if (!have()) return {}; 39 | 40 | if (!get("_horseryCrazyName")) { 41 | visitUrl("place.php?whichplace=town_right&action=town_horsery"); 42 | } 43 | 44 | return { 45 | "Mysticality Percent": Number(get("_horseryCrazyMys")), 46 | "Muscle Percent": Number(get("_horseryCrazyMus")), 47 | "Moxie Percent": Number(get("_horseryCrazyMox")), 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/resources/2006/CommaChameleon.ts: -------------------------------------------------------------------------------- 1 | import { $familiar, $item } from "../../template-string.js"; 2 | import { have as have_ } from "../../lib.js"; 3 | import { Familiar, familiarEquipment, toInt, visitUrl } from "kolmafia"; 4 | import { get } from "../../property.js"; 5 | 6 | const familiar = $familiar`Comma Chameleon`; 7 | 8 | /** 9 | * Determines whether the player has the Comma Chameleon in their 10 | * terrarium 11 | * 12 | * @returns Whether the player has a Comma Chameleon 13 | */ 14 | export function have(): boolean { 15 | return have_(familiar); 16 | } 17 | 18 | /** 19 | * Transforms Comma Chameleon into the familiar of choice if you already have the appropriate familiar equipment 20 | * Will not transform if you do not 21 | * @param fam determines what to transform into 22 | * @returns Whether Comma has been successfully transformed 23 | */ 24 | export function transform(fam: Familiar): boolean { 25 | if (currentFamiliar() === fam) { 26 | return true; 27 | } 28 | const equipment = familiarEquipment(fam); 29 | if (equipment === $item.none) return false; 30 | if (!have_(equipment)) return false; 31 | visitUrl( 32 | `inv_equip.php?which=2&action=equip&whichitem=${toInt(equipment)}&pwd`, 33 | ); 34 | visitUrl("charpane.php"); // This is the only way to get Mafia to update it's pref 35 | if (currentFamiliar() !== fam) { 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | /** 42 | * @returns The current familiar that Comma is behaving as 43 | */ 44 | export function currentFamiliar(): Familiar | null { 45 | return get("commaFamiliar"); 46 | } 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./actions/index.js"; 2 | export * from "./ascend.js"; 3 | export * from "./Clan.js"; 4 | export * from "./challengePaths/index.js"; 5 | export * from "./combat.js"; 6 | export * as Counter from "./counter.js"; 7 | export * from "./diet/index.js"; 8 | export * from "./Dungeon.js"; 9 | export * from "./lib.js"; 10 | export * from "./maximize.js"; 11 | export * as Mining from "./mining.js"; 12 | export { 13 | numericModifiers, 14 | booleanModifiers, 15 | stringModifiers, 16 | } from "./modifierTypes.js"; 17 | export * from "./mood.js"; 18 | export * from "./moonSign.js"; 19 | export * from "./propertyTypes.js"; 20 | export * from "./propertyTyping.js"; 21 | export * from "./resources/index.js"; 22 | export * from "./since.js"; 23 | export * from "./template-string.js"; 24 | export { default as Kmail } from "./Kmail.js"; 25 | export { default as logger } from "./logger.js"; 26 | export * as console from "./console.js"; 27 | export * as property from "./property.js"; 28 | export * as propertyTypes from "./propertyTypes.js"; 29 | export * from "./utils.js"; 30 | export { 31 | get, 32 | PropertiesManager, 33 | set, 34 | setProperties, 35 | withProperties, 36 | withProperty, 37 | withChoices, 38 | withChoice, 39 | } from "./property.js"; 40 | export { get as getModifier, getTotalModifier } from "./modifier.js"; 41 | export { Session } from "./session.js"; 42 | 43 | export { LogLevels } from "./logger.js"; 44 | export type { Modifiers } from "./modifier.js"; 45 | export type { 46 | NumericModifier, 47 | StringModifier, 48 | BooleanModifier, 49 | ModifierType, 50 | } from "./modifierTypes.js"; 51 | -------------------------------------------------------------------------------- /src/resources/2014/WinterGarden.ts: -------------------------------------------------------------------------------- 1 | import { Monster } from "kolmafia"; 2 | import { Copier } from "../../Copier.js"; 3 | import { haveInCampground, have as haveItem } from "../../lib.js"; 4 | import { get } from "../../property.js"; 5 | import { $item } from "../../template-string.js"; 6 | 7 | /** 8 | * @returns Whether the Winter Garden is our currently installed garden 9 | */ 10 | export function have(): boolean { 11 | return haveInCampground($item`packet of winter seeds`); 12 | } 13 | 14 | /** 15 | * @returns Whether we have an unfinished ice sculpture hanging around 16 | */ 17 | export function haveUnfinishedIceSculpture(): boolean { 18 | return haveItem($item`unfinished ice sculpture`); 19 | } 20 | 21 | /** 22 | * @returns Whether or not we've used an unfinished ice sculpture today 23 | */ 24 | export function isUnfinishedIceSculptureUsed(): boolean { 25 | return get("_iceSculptureUsed"); 26 | } 27 | 28 | /** 29 | * @returns Whether we're able to use an unfinished ice sculpture in combat right now 30 | */ 31 | export function couldUseUnfinishedIceSculpture(): boolean { 32 | return ( 33 | haveItem($item`unfinished ice sculpture`) && !haveItem($item`ice sculpture`) 34 | ); 35 | } 36 | 37 | /** 38 | * @returns Our current ice sculpture monster; `null` if none 39 | */ 40 | export function getUnfinishedIceSculptureMonster(): Monster | null { 41 | return get("iceSculptureMonster"); 42 | } 43 | 44 | export const UnfinishedIceSculpture = new Copier( 45 | () => couldUseUnfinishedIceSculpture(), 46 | null, 47 | () => couldUseUnfinishedIceSculpture(), 48 | () => getUnfinishedIceSculptureMonster(), 49 | ); 50 | -------------------------------------------------------------------------------- /src/resources/2012/ReagnimatedGnome.ts: -------------------------------------------------------------------------------- 1 | import { Item, runChoice, visitUrl } from "kolmafia"; 2 | import { have as _have } from "../../lib.js"; 3 | import { $familiar, $item } from "../../template-string.js"; 4 | 5 | /** 6 | * @returns Whether the player has a Reagnimated Gnome in their terrarium 7 | */ 8 | export function have(): boolean { 9 | return _have($familiar`Reagnimated Gnome`); 10 | } 11 | 12 | export const bodyParts = { 13 | ears: $item`gnomish swimmer's ears`, 14 | lung: $item`gnomish coal miner's lung`, 15 | elbow: $item`gnomish tennis elbow`, 16 | kgnee: $item`gnomish housemaid's kgnee`, 17 | foot: $item`gnomish athlete's foot`, 18 | } as const; 19 | 20 | export type BodyPart = keyof typeof bodyParts; 21 | 22 | /** 23 | * @returns Reagnimated Gnome parts that have already been retrieved from the arena 24 | */ 25 | export function chosenParts(): Item[] { 26 | return Object.values(bodyParts).filter((part) => _have(part)); 27 | } 28 | 29 | /** 30 | * Fetch Reagnimated Gnome part from the arena 31 | * 32 | * @param part Reagnimated Gnome body part 33 | * @returns Success 34 | */ 35 | export function choosePart(part: BodyPart): boolean { 36 | if (!have()) return false; 37 | if (_have(bodyParts[part])) return true; 38 | visitUrl("arena.php"); 39 | runChoice(4); 40 | return chosenParts().includes(bodyParts[part]); 41 | } 42 | 43 | /** 44 | * Calculate expected adventures from a given combat for a Reagnimated Gnome at a given weight 45 | * 46 | * @param weight Weight with which to calculuate 47 | * @returns Expected adventures 48 | */ 49 | export function expectedAdvsPerCombat(weight: number): number { 50 | return Math.min(0.01 + (weight / 1000) * 0.99, 1); 51 | } 52 | -------------------------------------------------------------------------------- /src/resources/evergreen/Raffle.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import { describe, it, expect, vi } from "vitest"; 4 | import { descToItem, Item, visitUrl } from "kolmafia"; 5 | 6 | import * as Raffle from "./Raffle.js"; 7 | 8 | describe("Raffle", () => { 9 | it("detects today's raffle prizes", async () => { 10 | const page = await fs.readFile( 11 | path.resolve(import.meta.dirname, "./__fixtures__/raffle.html"), 12 | "utf8", 13 | ); 14 | vi.mocked(visitUrl).mockImplementation(() => page); 15 | vi.mocked(descToItem).mockImplementation((descId: string) => { 16 | switch (descId) { 17 | case "500042705": 18 | // @ts-expect-error Constructing an item 19 | return new Item("Doll Moll violin case"); 20 | case "405271774": 21 | // @ts-expect-error Constructing an item 22 | return new Item("clan underground fireworks shop"); 23 | default: 24 | // @ts-expect-error Constructing an item 25 | return new Item("none"); 26 | } 27 | }); 28 | const prizes = Raffle.getRafflePrizes(); 29 | 30 | expect(prizes).toHaveLength(2); 31 | expect(prizes[0].name).toBe("clan underground fireworks shop"); 32 | expect(prizes[1].name).toBe("Doll Moll violin case"); 33 | }); 34 | 35 | it("detects the number of tickets purchased", async () => { 36 | const page = await fs.readFile( 37 | path.resolve(import.meta.dirname, "./__fixtures__/raffle.html"), 38 | "utf8", 39 | ); 40 | vi.mocked(visitUrl).mockImplementation(() => page); 41 | const tickets = Raffle.currentTickets(); 42 | expect(tickets).toEqual(111); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/counter.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, getCounter, getCounters } from "kolmafia"; 2 | 3 | /** 4 | * Returns Infinity for counters that do not exist, and otherwise returns the duration of the counter 5 | * 6 | * @param counter The name of the counter in question 7 | * @returns Infinity if the counter does not exist; otherwise returns the duration of the counter 8 | */ 9 | export function get(counter: string): number { 10 | const value = getCounter(counter); 11 | // getCounter returns -1 for counters that don't exist, but it also returns -1 for counters whose value is -1 12 | if (value === -1) { 13 | // if we have a counter with value -1, we check to see if that counter exists via getCounters() 14 | // We return null if it doesn't exist 15 | return getCounters(counter, -1, -1).trim() === "" ? Infinity : -1; 16 | } 17 | return value; 18 | } 19 | 20 | /** 21 | * The world is everything that is the case. This determines which counters are the case. 22 | * 23 | * @param counter The name of the counter in question 24 | * @returns True for counters which currently exist; false for those which do not 25 | */ 26 | export function exists(counter: string): boolean { 27 | return ( 28 | getCounter(counter) !== -1 || getCounters(counter, -1, -1).trim() !== "" 29 | ); 30 | } 31 | 32 | /** 33 | * Creates a manual counter with specified name and duration 34 | * 35 | * @param counter Name of the counter to manually create 36 | * @param duration Duration of counter to manually set 37 | * @returns Whether the counter was successfully set 38 | */ 39 | export function set(counter: string, duration: number): boolean { 40 | cliExecute(`counters add ${duration} ${counter}`); 41 | return get(counter) !== null; 42 | } 43 | -------------------------------------------------------------------------------- /src/resources/2023/CinchoDeMayo.ts: -------------------------------------------------------------------------------- 1 | import { totalFreeRests } from "kolmafia"; 2 | import { have as have_ } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $skill } from "../../template-string.js"; 5 | import { clamp, sum } from "../../utils.js"; 6 | 7 | const cincho = $item`Cincho de Mayo`; 8 | 9 | /** 10 | * @returns Whether you `have` the Cincho de Mayo 11 | */ 12 | export function have(): boolean { 13 | return have_(cincho); 14 | } 15 | 16 | /** 17 | * @returns Your current cinch 18 | */ 19 | export function currentCinch(): number { 20 | return have() ? clamp(100 - get("_cinchUsed"), 0, 100) : 0; 21 | } 22 | 23 | /** 24 | * @param currentRests The rest number to evaluate 25 | * @returns The amount of cinch restored by the given rest 26 | */ 27 | export function cinchRestoredBy(currentRests = get("_cinchoRests")) { 28 | return have() ? clamp(50 - currentRests * 5, 5, 30) : 0; 29 | } 30 | 31 | /** 32 | * @returns Your current cinch, plus the total amount if cinch that can be generated through free rests 33 | */ 34 | export function totalAvailableCinch(): number { 35 | const remainingRests = Math.max(0, totalFreeRests() - get("timesRested")); 36 | 37 | return have() 38 | ? currentCinch() + 39 | sum( 40 | new Array(remainingRests) 41 | .fill(null) 42 | .map((_, i) => i + get("_cinchoRests")), 43 | (restNumber) => cinchRestoredBy(restNumber), 44 | ) 45 | : 0; 46 | } 47 | 48 | export const skills = { 49 | SaltAndLime: $skill`Cincho: Dispense Salt and Lime`, 50 | PartySoundtrack: $skill`Cincho: Party Soundtrack`, 51 | FiestaExit: $skill`Cincho: Fiesta Exit`, 52 | ProjectilePiñata: $skill`Cincho: Projectile Piñata`, 53 | PartyFoul: $skill`Cincho: Party Foul`, 54 | ConfettiExtrava: $skill`Cincho: Confetti Extravaganza`, 55 | } as const; 56 | -------------------------------------------------------------------------------- /src/resources/evergreen/Raffle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | canAdventure, 3 | canInteract, 4 | cliExecute, 5 | descToItem, 6 | Location, 7 | myPath, 8 | Path, 9 | visitUrl, 10 | } from "kolmafia"; 11 | import { tuple } from "../../utils.js"; 12 | 13 | /** 14 | * @returns Whether the player can access the raffle house. 15 | */ 16 | export function accessible() { 17 | return ( 18 | canAdventure(Location.get("South of the Border")) && 19 | myPath() !== Path.get("Zombie Slayer") 20 | ); 21 | } 22 | 23 | function getRafflePrize(place: "First" | "Second", page: string) { 24 | const descId = new RegExp( 25 | `${place} Prize: couldUseRainDohBlackBox(), 40 | null, 41 | () => couldUseRainDohBlackBox(), 42 | () => getRainDohBlackBoxMonster(), 43 | () => useRainDohBlackBox(), 44 | ); 45 | 46 | /** 47 | * Determines whether you can still use a spooky putty sheet 48 | * 49 | * @returns Whether you can still use a spooky putty sheet 50 | */ 51 | export function couldUseSpookyPuttySheet(): boolean { 52 | return ( 53 | haveSpookyPutty() && 54 | getSpookyPuttySheetCopiesMade() < 5 && 55 | getTotalPuttyLikeCopiesMade() < 6 56 | ); 57 | } 58 | 59 | export const SpookyPuttySheet = new Copier( 60 | () => couldUseSpookyPuttySheet(), 61 | () => prepareSpookyPuttySheet(), 62 | () => couldUseSpookyPuttySheet(), 63 | () => getSpookyPuttySheetMonster(), 64 | () => useSpookyPuttySheet(), 65 | ); 66 | -------------------------------------------------------------------------------- /src/resources/2015/MayoClinic.ts: -------------------------------------------------------------------------------- 1 | import { buy, getWorkshed, Item, retrieveItem, use } from "kolmafia"; 2 | import { have as haveItem } from "../../lib.js"; 3 | import logger from "../../logger.js"; 4 | import { get, withChoice } from "../../property.js"; 5 | import { $item } from "../../template-string.js"; 6 | 7 | export const Mayo = { 8 | nex: $item`Mayonex`, 9 | diol: $item`Mayodiol`, 10 | zapine: $item`Mayozapine`, 11 | flex: $item`Mayoflex`, 12 | }; 13 | 14 | /** 15 | * @returns Whether the Mayo Clinic is our current active workshed 16 | */ 17 | export function installed(): boolean { 18 | return getWorkshed() === $item`portable Mayo Clinic`; 19 | } 20 | 21 | /** 22 | * @returns Whether we `have` the Mayo Clinic, or it's installed 23 | */ 24 | export function have(): boolean { 25 | return haveItem($item`portable Mayo Clinic`) || installed(); 26 | } 27 | 28 | /** 29 | * Sets mayo minder to a particular mayo, and ensures you have enough of it. 30 | * 31 | * @param mayo Mayo to use 32 | * @param quantity Quantity to ensure 33 | * @returns Whether we succeeded in this endeavor; a trivial `false` for people without the clinic `installed` 34 | */ 35 | export function setMayoMinder(mayo: Item, quantity = 1): boolean { 36 | if (getWorkshed() !== $item`portable Mayo Clinic`) return false; 37 | if (!Object.values(Mayo).includes(mayo)) { 38 | logger.error("Invalid mayo selected"); 39 | return false; 40 | } 41 | if (get("mayoInMouth") && get("mayoInMouth") !== mayo.name) { 42 | logger.error("Currently have incorrect mayo in mouth"); 43 | return false; 44 | } 45 | retrieveItem(quantity, mayo); 46 | if (!haveItem($item`Mayo Minder™`)) buy($item`Mayo Minder™`); 47 | if (get("mayoMinderSetting") !== mayo.name) { 48 | withChoice(1076, mayo.id - 8260, () => use($item`Mayo Minder™`)); 49 | } 50 | return get("mayoMinderSetting") === mayo.name; 51 | } 52 | -------------------------------------------------------------------------------- /src/resources/2025/ToyCupidBow.ts: -------------------------------------------------------------------------------- 1 | import { Familiar, toFamiliar } from "kolmafia"; 2 | import { have as have_ } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item } from "../../template-string.js"; 5 | import { clamp } from "../../utils.js"; 6 | 7 | /** 8 | * @returns Whether you `have` the Toy Cupid's bow 9 | */ 10 | export function have(): boolean { 11 | return have_($item`toy Cupid bow`); 12 | } 13 | 14 | /** 15 | * @returns The current familiar your cupid bow is getting the equip of (null if there isn't one) 16 | */ 17 | export function currentFamiliar(): Familiar | null { 18 | return get("cupidBowLastFamiliar"); 19 | } 20 | 21 | /** 22 | * @returns An array of familiars who have received their drops from the cupid bow today 23 | */ 24 | export function familiarsToday(): Familiar[] { 25 | return get("_cupidBowFamiliars") 26 | .split(";") 27 | .map((id) => toFamiliar(Number(id))); 28 | } 29 | 30 | /** 31 | * Determine whether you've obtained the equipment of a given familiar with the TCB today 32 | * @param familiar The familiar in question 33 | * @returns Whether you've obtained the equipment from that familiar via the TCB today 34 | */ 35 | export function doneToday(familiar: Familiar): boolean { 36 | return RegExp(`(?:^|;)${familiar.id}(?:$|;)`).test(get("_cupidBowFamiliars")); 37 | } 38 | 39 | /** 40 | * Calculate the number of fights needed to get a drop 41 | * @param familiar The familiar in question; defaults to the one currently charged by your bow 42 | * @returns The number of turns necessary to get this familiar's drop; Infinity if you can't get it today 43 | */ 44 | export function turnsLeft(familiar = currentFamiliar()): number { 45 | if ((familiarsToday() as (Familiar | null)[]).includes(familiar)) 46 | return Infinity; 47 | if (currentFamiliar() !== familiar) return 5; 48 | return clamp(5 - get("cupidBowFights"), 1, 5); 49 | } 50 | -------------------------------------------------------------------------------- /src/resources/2025/SkeletonOfCrimboPast.ts: -------------------------------------------------------------------------------- 1 | import { Location } from "kolmafia"; 2 | import { 3 | AdventureTarget, 4 | adventureTargetToWeightedMap, 5 | have as have_, 6 | } from "../../lib.js"; 7 | import { $familiar, $phylum } from "../../template-string.js"; 8 | import { get } from "../../property.js"; 9 | import { sum } from "../../utils.js"; 10 | 11 | const BONE_PHYLA = new Map([ 12 | [$phylum`beast`, 0.3], 13 | [$phylum`bug`, 0.05], 14 | [$phylum`construct`, 0.1], 15 | [$phylum`demon`, 0.4], 16 | [$phylum`elf`, 0.5], 17 | [$phylum`fish`, 0.15], 18 | [$phylum`goblin`, 0.4], 19 | [$phylum`hobo`, 0.5], 20 | [$phylum`humanoid`, 0.4], 21 | [$phylum`orc`, 0.7], 22 | [$phylum`penguin`, 0.2], 23 | [$phylum`pirate`, 0.7], 24 | [$phylum`dude`, 0.5], 25 | [$phylum`undead`, 0.3], 26 | [$phylum`weird`, 0.2], 27 | ]); 28 | 29 | /** 30 | * @returns Whether or not you have the Skeleton of Crimbo Past. 31 | */ 32 | export function have(): boolean { 33 | return have_($familiar`Skeleton of Crimbo Past`); 34 | } 35 | 36 | /** 37 | * @param target The target you expect to fight; accepts Monster, Location, or map of 38 | * @returns The odds of getting a knucklebone drop 39 | */ 40 | export function expectedBones(target: AdventureTarget): number { 41 | if (!have()) return 0; 42 | if (get("_knuckleboneDrops") >= 100) return 0; 43 | if (target instanceof Location) { 44 | return expectedBones(adventureTargetToWeightedMap(target)); 45 | } 46 | if (target instanceof Map) { 47 | return sum( 48 | [...target.entries()], 49 | ([monster, rate]) => rate * expectedBones(monster), 50 | ); 51 | } 52 | 53 | if (target.attributes.includes("SKELETON")) return 0.9; 54 | 55 | return BONE_PHYLA.get(target.phylum) ?? 0; 56 | } 57 | 58 | /** 59 | * Returns the standard 10% improvement the can adds; in case scripts find this helpful over a magic number 60 | */ 61 | export const CANE_BONUS = 0.1; 62 | -------------------------------------------------------------------------------- /src/resources/2016/Witchess.ts: -------------------------------------------------------------------------------- 1 | import { Monster, myHash, runChoice, runCombat, visitUrl } from "kolmafia"; 2 | import { CombatParams, haveInCampground } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item } from "../../template-string.js"; 5 | 6 | const item = $item`Witchess Set`; 7 | /** 8 | * @returns Is the Witchess installed and available in our campground? 9 | */ 10 | export function have(): boolean { 11 | return haveInCampground(item); 12 | } 13 | 14 | /** 15 | * @returns How many Witchess fights have we done so far today? 16 | */ 17 | export function fightsDone(): number { 18 | return get("_witchessFights"); 19 | } 20 | 21 | export const pieces = Monster.get([ 22 | "Witchess Pawn", 23 | "Witchess Knight", 24 | "Witchess Bishop", 25 | "Witchess Rook", 26 | "Witchess Queen", 27 | "Witchess King", 28 | "Witchess Witch", 29 | "Witchess Ox", 30 | ]); 31 | /** 32 | * Fight a Witchess piece of your choice 33 | * 34 | * @param piece The piece to fight 35 | * @param combatParams Any parameters you'd like to pass to `runCombat` 36 | * @returns The value of `runCombat()`, which is the page html of the final round 37 | */ 38 | export function fightPiece( 39 | piece: Monster, 40 | ...combatParams: CombatParams 41 | ): string { 42 | if (!pieces.includes(piece)) throw new Error("That is not a valid piece."); 43 | if ( 44 | !visitUrl("campground.php?action=witchess").includes( 45 | "whichchoice value=1181", 46 | ) 47 | ) { 48 | throw new Error("Failed to open Witchess."); 49 | } 50 | if (!runChoice(1).includes("whichchoice=1182")) { 51 | throw new Error("Failed to visit shrink ray."); 52 | } 53 | if ( 54 | !visitUrl( 55 | `choice.php?option=1&pwd=${myHash()}&whichchoice=1182&piece=${piece.id}`, 56 | false, 57 | ).includes(piece.name) 58 | ) { 59 | throw new Error("Failed to start fight."); 60 | } 61 | return runCombat(...combatParams); 62 | } 63 | -------------------------------------------------------------------------------- /src/resources/2018/SongBoom.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute } from "kolmafia"; 2 | import { have as haveItem } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item } from "../../template-string.js"; 5 | 6 | const item = $item`SongBoom™ BoomBox`; 7 | /** 8 | * @returns Whether we `have` the SongBoom™ BoomBox 9 | */ 10 | export function have(): boolean { 11 | return haveItem(item); 12 | } 13 | 14 | const keywords = { 15 | "Eye of the Giger": "spooky", 16 | "Food Vibrations": "food", 17 | "Remainin' Alive": "dr", 18 | "These Fists Were Made for Punchin'": "damage", 19 | "Total Eclipse of Your Meat": "meat", 20 | }; 21 | 22 | export type SongBoomSong = keyof typeof keywords | null; 23 | 24 | export const songBoomSongs = new Set(Object.keys(keywords)); 25 | 26 | /** 27 | * @returns The `SongBoomSong` you currently have active; `null` if none is active at this time 28 | */ 29 | export function song(): SongBoomSong { 30 | const stored = get("boomBoxSong"); 31 | return songBoomSongs.has(stored) ? (stored as SongBoomSong) : null; 32 | } 33 | 34 | /** 35 | * @returns Song changes left today. 36 | */ 37 | export function songChangesLeft(): number { 38 | return get("_boomBoxSongsLeft"); 39 | } 40 | 41 | /** 42 | * Change the song. Throws an error if unable. 43 | * 44 | * @param newSong Song to change to. 45 | * @returns Whether we successfully changed the song; `false` thus means that this was already our current song. 46 | */ 47 | export function setSong(newSong: SongBoomSong): boolean { 48 | if (song() !== newSong) { 49 | if (songChangesLeft() === 0) throw new Error("Out of song changes!"); 50 | cliExecute(`boombox ${newSong ? keywords[newSong] : "none"}`); 51 | return true; 52 | } else { 53 | return false; 54 | } 55 | } 56 | 57 | /** 58 | * @returns Progress to next song drop (e.g. gathered meat-clip). 59 | */ 60 | export function dropProgress(): number { 61 | return get("_boomBoxFights"); 62 | } 63 | -------------------------------------------------------------------------------- /src/resources/2025/CrepeParachute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | handlingChoice, 3 | lastChoice, 4 | Monster, 5 | runChoice, 6 | runCombat, 7 | toMonster, 8 | visitUrl, 9 | xpath, 10 | } from "kolmafia"; 11 | import { CombatParams, have as have_ } from "../../lib.js"; 12 | import { $effect, $item } from "../../template-string.js"; 13 | import { Delayed, undelay } from "../../utils.js"; 14 | 15 | /** 16 | * @returns Whether or not you have the crepe paper parachute cape 17 | */ 18 | export function have(): boolean { 19 | return have_($item`crepe paper parachute cape`); 20 | } 21 | 22 | const visitParachute = () => visitUrl("inventory.php?action=parachute&pwd"); 23 | 24 | function checkMonsters(html: string): Monster[] { 25 | return xpath( 26 | html, 27 | "//select[@name='monid']//option[position()>1]/@value", 28 | ).map((id) => toMonster(Number(id))); 29 | } 30 | 31 | /** 32 | * @returns An array of monsters currently available in the Parachute. 33 | */ 34 | export function availableMonsters(): Monster[] { 35 | if (!have() || have_($effect`Everything looks Beige`)) return []; 36 | return checkMonsters(visitParachute()); 37 | } 38 | 39 | /** 40 | * @param target Either the monster we want to fight or a function for choosing said monster 41 | * @param combatParams Any parameters you'd like to pass to `runCombat` 42 | * @returns whether we successfully parachuted into the target monster 43 | */ 44 | export function fight( 45 | target: Delayed, 46 | ...combatParams: CombatParams 47 | ): boolean { 48 | if (!have()) return false; 49 | if (have_($effect`Everything looks Beige`)) return false; 50 | const monsters = checkMonsters(visitParachute()); 51 | const monster = undelay(target, monsters); 52 | if (!monsters.includes(monster)) return false; 53 | if (!handlingChoice() || lastChoice() !== 1543) visitParachute(); 54 | runChoice(1, `monid=${monster.id}`); 55 | runCombat(...combatParams); 56 | return true; 57 | } 58 | -------------------------------------------------------------------------------- /src/resources/2021/CrystalBall.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Location, 3 | Monster, 4 | toLocation, 5 | toMonster, 6 | availableAmount, 7 | Item, 8 | visitUrl, 9 | myTotalTurnsSpent, 10 | totalTurnsPlayed, 11 | } from "kolmafia"; 12 | import { canVisitUrl, multiSplit } from "../../lib.js"; 13 | import logger from "../../logger.js"; 14 | import { get } from "../../property.js"; 15 | 16 | export const orb = Item.get("miniature crystal ball"); 17 | /** 18 | * Determines whether you `have` the Miniature Crystal Ball 19 | * 20 | * @returns Whether you `have` the Miniature Crystal Ball 21 | */ 22 | export function have(): boolean { 23 | return availableAmount(orb) > 0; 24 | } 25 | 26 | /** 27 | * @returns A map keyed by locations and whose values are monsters, representing all active orb predictions 28 | */ 29 | export function getPrediction(): Map { 30 | return new Map( 31 | multiSplit("crystalBallPredictions", "|", ":", [ 32 | Number, 33 | toLocation, 34 | toMonster, 35 | ]).map(([, location, monster]) => [location, monster]), 36 | ); 37 | } 38 | 39 | const getLastPondered = () => 40 | `${myTotalTurnsSpent()};${totalTurnsPlayed()};${get("lastAdventure")}`; 41 | let lastPondered = ""; 42 | 43 | /** 44 | * Ponders your orb (if it is able to do so safely) and then returns a Map keyed by location consisting of extant predictions 45 | * 46 | * @returns A map of all predictions currently active in an adventurer's miniature crystal ball, after visiting the "ponder" URL to refresh them. 47 | */ 48 | export function ponder(): Map { 49 | if (!have()) return new Map(); 50 | if (lastPondered !== getLastPondered()) { 51 | if (canVisitUrl()) { 52 | logger.debug("Now pondering Crystal Ball."); 53 | visitUrl("inventory.php?ponder=1", false); 54 | lastPondered = getLastPondered(); 55 | } else { 56 | logger.debug("Failed to ponder Crystall Ball."); 57 | } 58 | } 59 | return getPrediction(); 60 | } 61 | -------------------------------------------------------------------------------- /src/resources/2011/ObtuseAngel.ts: -------------------------------------------------------------------------------- 1 | import { Monster, useFamiliar } from "kolmafia"; 2 | import { Copier } from "../../Copier.js"; 3 | import { have as _have, isCurrentFamiliar } from "../../lib.js"; 4 | import { get } from "../../property.js"; 5 | import { $familiar } from "../../template-string.js"; 6 | 7 | const familiar = $familiar`Obtuse Angel`; 8 | 9 | /** 10 | * @returns whether the player has an Obtuse Angel 11 | */ 12 | export function have(): boolean { 13 | return _have(familiar); 14 | } 15 | 16 | /** 17 | * @returns number of badly romantic arrows used today 18 | */ 19 | export function getBadlyRomanticArrowUses(): number { 20 | return Math.max(0, get("_badlyRomanticArrows")); 21 | } 22 | 23 | /** 24 | * @returns whether badly romantic arrow can still be used 25 | */ 26 | export function haveBadlyRomanticArrowUsesRemaining(): boolean { 27 | return getBadlyRomanticArrowUses() === 0; 28 | } 29 | 30 | /** 31 | * @returns whether the player could use badly romantic arrow in theory 32 | */ 33 | export function couldUseBadlyRomanticArrow(): boolean { 34 | return have() && haveBadlyRomanticArrowUsesRemaining(); 35 | } 36 | 37 | /** 38 | * Prepares badly romantic arrow for use 39 | * 40 | * @returns success 41 | */ 42 | export function prepareBadlyRomanticArrow(): boolean { 43 | return useFamiliar(familiar); 44 | } 45 | 46 | /** 47 | * @returns whether the player can use badly romantic arrow right now 48 | */ 49 | export function canUseBadlyRomanticArrow(): boolean { 50 | return isCurrentFamiliar(familiar) && haveBadlyRomanticArrowUsesRemaining(); 51 | } 52 | 53 | /** 54 | * @returns current badly romantic arrow monster target 55 | */ 56 | export function getBadlyRomanticArrowMonster(): Monster | null { 57 | return get("romanticTarget"); 58 | } 59 | 60 | export const BadlyRomanticArrow = new Copier( 61 | () => couldUseBadlyRomanticArrow(), 62 | () => prepareBadlyRomanticArrow(), 63 | () => canUseBadlyRomanticArrow(), 64 | () => getBadlyRomanticArrowMonster(), 65 | ); 66 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | env: 9 | node-version: "24" 10 | path: "KoLmafia" 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: corepack enable 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ env.node-version }} 21 | - name: Install modules 22 | run: yarn install --immutable 23 | - name: Run ESLint & Prettier 24 | run: yarn run lint 25 | 26 | testBuild: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - run: corepack enable 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ env.node-version }} 34 | - name: Install modules 35 | run: yarn install --immutable 36 | - name: Build 37 | run: yarn run build 38 | 39 | tests: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - run: corepack enable 44 | - uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ env.node-version }} 47 | - name: Install modules 48 | run: yarn install --immutable 49 | - name: Run tests 50 | run: yarn run test 51 | 52 | push: 53 | runs-on: ubuntu-latest 54 | needs: [lint, testBuild, tests] 55 | if: github.ref == 'refs/heads/main' 56 | 57 | steps: 58 | - uses: actions/checkout@v4 59 | - run: corepack enable 60 | - uses: actions/setup-node@v4 61 | with: 62 | node-version: ${{ env.node-version }} 63 | - name: Install modules 64 | run: yarn install --immutable 65 | - name: Build 66 | run: yarn run build 67 | 68 | - name: Push to Release 69 | uses: s0/git-publish-subdir-action@develop 70 | env: 71 | REPO: self 72 | BRANCH: release 73 | FOLDER: ${{ env.path }} 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | MESSAGE: "Build: ({sha}) {msg}" 76 | SKIP_EMPTY_COMMITS: true 77 | -------------------------------------------------------------------------------- /src/resources/2020/Cartography.ts: -------------------------------------------------------------------------------- 1 | import { 2 | canAdventure, 3 | currentRound, 4 | handlingChoice, 5 | lastChoice, 6 | Location, 7 | Monster, 8 | myTurncount, 9 | runChoice, 10 | toUrl, 11 | useSkill, 12 | visitUrl, 13 | } from "kolmafia"; 14 | import { have as _have } from "../../lib.js"; 15 | import { get } from "../../property.js"; 16 | import { $skill } from "../../template-string.js"; 17 | 18 | const passive = $skill`Comprehensive Cartography`; 19 | 20 | /** 21 | * Determines whether you `have` the skill Comprehensive Cartography 22 | * 23 | * @returns Whether you currently `have` the skill 24 | */ 25 | export function have(): boolean { 26 | return _have(passive); 27 | } 28 | 29 | /** 30 | * Map a particular monster in a particular location 31 | * You'll need to set your autoattack or CCS in advance of using this. Additionally, it will loop to try to avoid time-spinner pranks or zone intro adventures 32 | * 33 | * @param location The location to target 34 | * @param monster The monster to target 35 | * @returns Whether we successfully mapped the monster 36 | */ 37 | export function mapMonster(location: Location, monster: Monster): boolean { 38 | if (!have()) return false; 39 | if (get("_monstersMapped") >= 3) return false; 40 | if (!canAdventure(location)) return false; 41 | 42 | useSkill($skill`Map the Monsters`); 43 | if (!get("mappingMonsters")) return false; 44 | 45 | const turns = myTurncount(); 46 | while (currentRound() < 1) { 47 | // Not in combat 48 | if (myTurncount() > turns) { 49 | throw new Error("Map the Monsters unsuccessful?"); 50 | } 51 | visitUrl(toUrl(location)); 52 | if (handlingChoice() && lastChoice() === 1435) { 53 | runChoice(1, `heyscriptswhatsupwinkwink=${monster.id}`); 54 | return true; 55 | } else { 56 | runChoice(-1, false); 57 | } 58 | } 59 | return false; 60 | } 61 | 62 | /** 63 | * @returns The number of monsters you can map today 64 | */ 65 | export function availableMaps(): number { 66 | return have() ? $skill`Map the Monsters`.dailylimit : 0; 67 | } 68 | 69 | /** 70 | * @returns Whether or not `Map the Monsters` is currently active 71 | */ 72 | export function currentlyMapping(): boolean { 73 | return get("mappingMonsters"); 74 | } 75 | -------------------------------------------------------------------------------- /src/resources/2024/TearawayPants.ts: -------------------------------------------------------------------------------- 1 | import { myClass, cliExecute, equip, visitUrl } from "kolmafia"; 2 | import { have as have_, questStep } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $classes, $item, $slot } from "../../template-string.js"; 5 | 6 | const item = $item`tearaway pants`; 7 | /** 8 | * @returns Whether you `have` the tearaway pants 9 | */ 10 | export function have(): boolean { 11 | return have_(item); 12 | } 13 | 14 | /** 15 | * Calculate the chance of getting adventures from a fight against plants 16 | * @param advs The number of adventures to calculate the probability at; defaults to the current value 17 | * @returns The likelihood of getting an adventure from ripping off your pants against plants 18 | */ 19 | export function plantsAdventureChance( 20 | advs = get("_tearawayPantsAdvs"), 21 | ): number { 22 | return 1 / (2 + advs); 23 | } 24 | 25 | /** 26 | * Calculate the expected total number of pant-plant adventures you'll gain over a period 27 | * @param turnsToSpend The total number of plant-combats you expect to spend 28 | * @param startingAdvs The number of pant-plants adventures to start with--defaults to the current value 29 | * @returns The expected total number of adventures to gain over the period 30 | */ 31 | export function expectedTotalAdventures( 32 | turnsToSpend: number, 33 | startingAdvs = get("_tearawayPantsAdvs"), 34 | ): number { 35 | return ( 36 | (1 - 37 | 2 * startingAdvs + 38 | Math.sqrt( 39 | 4 * startingAdvs ** 2 - 4 * startingAdvs + 1 + 8 * turnsToSpend, 40 | )) / 41 | 2 42 | ); 43 | } 44 | 45 | /** 46 | * Attempt to unlock the moxie guild--for free--using these incredible pants 47 | * @returns Whether we've successfully unlocked the moxie guild 48 | */ 49 | export function unlockGuild(): boolean { 50 | if (!$classes`Disco Bandit, Accordion Thief`.includes(myClass())) 51 | return false; 52 | if (questStep("questG08Moxie") >= 999) return true; 53 | if (!have()) return false; 54 | try { 55 | cliExecute("checkpoint"); 56 | equip($slot`pants`, item); 57 | visitUrl("guild.php?place=challenge"); 58 | } finally { 59 | cliExecute("outfit checkpoint"); 60 | } 61 | 62 | return questStep("questG08Moxie") >= 999; 63 | } 64 | -------------------------------------------------------------------------------- /src/resources/2024/BatWings.ts: -------------------------------------------------------------------------------- 1 | import { equip, equippedItem, visitUrl } from "kolmafia"; 2 | import { have as have_, questStep } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $skill, $slot } from "../../template-string.js"; 5 | 6 | /** 7 | * @returns Whether you `have` the Bat Wings 8 | */ 9 | export function have(): boolean { 10 | return have_($item`bat wings`); 11 | } 12 | 13 | /** 14 | * @returns The number of times you can swoop like a bat today 15 | */ 16 | export function swoopsRemaining(): number { 17 | return have() ? $skill`Swoop like a Bat`.dailylimit : 0; 18 | } 19 | 20 | /** 21 | * @returns The number of times you can rest upside down today 22 | */ 23 | export function restUpsideDownRemaining(): number { 24 | return have() ? $skill`Rest upside down`.dailylimit : 0; 25 | } 26 | 27 | /** 28 | * @returns The number of times you can summon a cauldron of bats today 29 | */ 30 | export function cauldronsRemaining(): number { 31 | return have() ? $skill`Summon Cauldron of Bats`.dailylimit : 0; 32 | } 33 | 34 | /** 35 | * Calculates the chance of getting a free fight with the Bat Wings equipped 36 | * @param flaps The number of times your wings have _already_ granted a free fight for the purposes of this calculation; defaults to its current value 37 | * @returns The chance of getting a free fight 38 | */ 39 | export function flapChance(flaps = get("_batWingsFreeFights")): number { 40 | return flaps < 5 ? 1 / (2 + flaps) : 0; 41 | } 42 | 43 | /** 44 | * Attempts to do bridge skip 45 | * @returns whether we were successfully able to skip the bridge 46 | */ 47 | export function jumpBridge(): boolean { 48 | if ( 49 | get("chasmBridgeProgress") < 25 || 50 | questStep("questL09Topping") === 0 || 51 | !have() 52 | ) { 53 | return false; 54 | } 55 | if (get("chasmBridgeProgress") === 30 || questStep("questL09Topping") >= 1) { 56 | return true; 57 | } 58 | const back = equippedItem($slot`back`); 59 | equip($item`bat wings`); 60 | visitUrl("place.php?whichplace=orc_chasm&action=bridge_jump"); // Jump the bridge 61 | visitUrl("place.php?whichplace=highlands&action=highlands_dude"); // Tell mafia you jumped the bridge 62 | equip(back); 63 | return questStep("questL09Topping") >= 2; // have spoken to the highland lord 64 | } 65 | -------------------------------------------------------------------------------- /src/resources/2019/PocketProfessor.ts: -------------------------------------------------------------------------------- 1 | import { haveEquipped } from "kolmafia"; 2 | import { have as have_, totalFamiliarWeight } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $familiar, $item } from "../../template-string.js"; 5 | 6 | const familiar = $familiar`Pocket Professor`; 7 | 8 | /** 9 | * @returns Whether you `have` the Pocket Professor 10 | */ 11 | export function have(): boolean { 12 | return have_(familiar); 13 | } 14 | 15 | /** 16 | * @returns Whether or not you're currently able to `Deliver your Thesis` 17 | */ 18 | export function canThesis(): boolean { 19 | return have() && familiar.experience >= 400 && !get("_thesisDelivered"); 20 | } 21 | 22 | /** 23 | * Calculate the total number of lectures available to you given a particular familiar weight and chip-equipped status 24 | * @param weight The weight to calculate at--defaults to your current total familiar weight 25 | * @param includeChip Whether or not to include the memory chip--defaults to whether or not it's currently equipped 26 | * @returns The total number of lectures you're able to deliver, including ones you've already delivered today 27 | */ 28 | export function totalAvailableLectures( 29 | weight = totalFamiliarWeight(familiar), 30 | includeChip = haveEquipped($item`Pocket Professor memory chip`), 31 | ): number { 32 | return (includeChip ? 2 : 0) + Math.floor(Math.sqrt(weight - 1)); 33 | } 34 | 35 | /** 36 | * @returns The number of Pocket Professor lectures you've delivered today 37 | */ 38 | export function lecturesDelivered(): number { 39 | return get("_pocketProfessorLectures"); 40 | } 41 | 42 | /** 43 | * Calculate the number of unused lectures available to you given a particular familiar weight and chip-equipped status 44 | * @param weight The weight to calculate at--defaults to your current total familiar weight 45 | * @param includeChip Whether or not to include the memory chip--defaults to whether or not it's currently equipped 46 | * @returns The number of lectures you're able to deliver, accounting for any you've already delivered today 47 | */ 48 | export function currentlyAvailableLectures( 49 | weight = totalFamiliarWeight(familiar), 50 | includeChip = haveEquipped($item`Pocket Professor memory chip`), 51 | ): number { 52 | return totalAvailableLectures(weight, includeChip) - lecturesDelivered(); 53 | } 54 | -------------------------------------------------------------------------------- /src/resources/2011/StompingBoots.ts: -------------------------------------------------------------------------------- 1 | import { useFamiliar } from "kolmafia"; 2 | import { 3 | have as _have, 4 | isCurrentFamiliar, 5 | totalFamiliarWeight, 6 | } from "../../lib.js"; 7 | import { get } from "../../property.js"; 8 | import { $familiar } from "../../template-string.js"; 9 | 10 | const familiar = $familiar`Pair of Stomping Boots`; 11 | 12 | /** 13 | * @returns whether the player has the Pair of Stomping Boots in their terrarium 14 | */ 15 | export function have(): boolean { 16 | return _have(familiar); 17 | } 18 | 19 | /** 20 | * @returns number of free runaways that have already been used today 21 | * @see Bandersnatch with which the Stomping Boots shares a counter 22 | */ 23 | export function getRunaways(): number { 24 | return get("_banderRunaways"); 25 | } 26 | 27 | /** 28 | * @param considerWeightAdjustment Include familiar weight modifiers 29 | * @returns total number of free runaways that the player can get from their Stomping Boots 30 | */ 31 | export function getMaxRunaways(considerWeightAdjustment = true): number { 32 | return Math.floor( 33 | totalFamiliarWeight(familiar, considerWeightAdjustment) / 5, 34 | ); 35 | } 36 | 37 | /** 38 | * @param considerWeightAdjustment Whether to consider weight adjustment in free run calculation 39 | * @returns the number of remaining free runaways the player can get from their Stomping Boots 40 | */ 41 | export function getRemainingRunaways(considerWeightAdjustment = true): number { 42 | return Math.max(0, getMaxRunaways(considerWeightAdjustment) - getRunaways()); 43 | } 44 | 45 | /** 46 | * @param considerWeightAdjustment Include familiar weight modifiers 47 | * @returns whether the player could use their Stomping Boots to get a free run in theory 48 | */ 49 | export function couldRunaway(considerWeightAdjustment = true): boolean { 50 | return have() && getRemainingRunaways(considerWeightAdjustment) > 0; 51 | } 52 | 53 | /** 54 | * @returns whether the player can use their Stomping Boots to get a free run right now 55 | */ 56 | export function canRunaway(): boolean { 57 | return isCurrentFamiliar(familiar) && couldRunaway(); 58 | } 59 | 60 | /** 61 | * Prepare a Stomping Boots runaway. 62 | * 63 | * This will take your Stomping Boots with you. 64 | * If any of those steps fail, it will return false. 65 | * 66 | * @returns Success 67 | */ 68 | export function prepareRunaway(): boolean { 69 | return useFamiliar(familiar); 70 | } 71 | -------------------------------------------------------------------------------- /src/resources/2023/AugustScepter.ts: -------------------------------------------------------------------------------- 1 | import { Skill, toSkill } from "kolmafia"; 2 | import { gameDay, have as have_ } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $skills } from "../../template-string.js"; 5 | import { Range } from "../../utils.js"; 6 | 7 | /** 8 | * @returns Whether you `have` the august scepter 9 | */ 10 | export function have(): boolean { 11 | return have_($item`august scepter`); 12 | } 13 | 14 | export const SKILLS = Object.freeze( 15 | $skills`Aug. 1st: Mountain Climbing Day!, Aug. 2nd: Find an Eleven-Leaf Clover Day, Aug. 3rd: Watermelon Day!, Aug. 4th: Water Balloon Day!, Aug. 5th: Oyster Day!, Aug. 6th: Fresh Breath Day!, Aug. 7th: Lighthouse Day!, Aug. 8th: Cat Day!, Aug. 9th: Hand Holding Day!, Aug. 10th: World Lion Day!, Aug. 11th: Presidential Joke Day!, Aug. 12th: Elephant Day!, Aug. 13th: Left/Off Hander's Day!, Aug. 14th: Financial Awareness Day!, Aug. 15th: Relaxation Day!, Aug. 16th: Roller Coaster Day!, Aug. 17th: Thriftshop Day!, Aug. 18th: Serendipity Day!, Aug. 19th: Honey Bee Awareness Day!, Aug. 20th: Mosquito Day!, Aug. 21st: Spumoni Day!, Aug. 22nd: Tooth Fairy Day!, Aug. 23rd: Ride the Wind Day!, Aug. 24th: Waffle Day!, Aug. 25th: Banana Split Day!, Aug. 26th: Toilet Paper Day!, Aug. 27th: Just Because Day!, Aug. 28th: Race Your Mouse Day!, Aug. 29th: More Herbs\, Less Salt Day!, Aug. 30th: Beach Day!, Aug. 31st: Cabernet Sauvignon Day!`, 16 | ); 17 | 18 | /** 19 | * @returns Today's august scepter skill 20 | */ 21 | export function todaysSkill(): Skill { 22 | return toSkill((gameDay().getDate() + 7451).toFixed(0)); 23 | } 24 | 25 | /** 26 | * @param skillNum the Day of the skill you wish to check 27 | * @returns Whether we have cast this skill yet today 28 | */ 29 | export function getAugustCast(skillNum: Range<1, 32>): boolean { 30 | return get(`_aug${skillNum}Cast`); 31 | } 32 | 33 | /** 34 | * @returns whether you have cast Today's august scepter skill 35 | */ 36 | export function getTodayCast(): boolean { 37 | return get("_augTodayCast"); 38 | } 39 | 40 | /** 41 | * @param skillNum the Day of the skill you wish to check 42 | * @returns Whether we can cast this skill 43 | */ 44 | export function canCast(skillNum: Range<1, 32>): boolean { 45 | return ( 46 | have() && 47 | !get(`_aug${skillNum}Cast`) && 48 | ((gameDay().getDate() === skillNum && !getTodayCast()) || 49 | get(`_augSkillsCast`) < 5) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { logprint, printHtml } from "kolmafia"; 2 | export enum LogLevels { 3 | NONE = 0, 4 | ERROR = 1, 5 | WARNING = 2, 6 | INFO = 3, 7 | DEBUG = 4, 8 | } 9 | 10 | const defaultHandlers = { 11 | [LogLevels.INFO]: (message: string): unknown => { 12 | printHtml(`[Libram Info] ${message}`); 13 | logprint(`[Libram] ${message}`); 14 | return; 15 | }, 16 | [LogLevels.WARNING]: (message: string): unknown => { 17 | printHtml( 18 | `[Libram Warning] ${message}`, 19 | ); 20 | logprint(`[Libram] ${message}`); 21 | return; 22 | }, 23 | [LogLevels.ERROR]: (error: string | Error): unknown => { 24 | printHtml( 25 | `[Libram Error] ${error.toString()}`, 26 | ); 27 | logprint(`[Libram] ${error}`); 28 | return; 29 | }, 30 | [LogLevels.DEBUG]: (message: string): unknown => { 31 | printHtml( 32 | `[Libram Debug] ${message}`, 33 | ); 34 | logprint(`[Libram] ${message}`); 35 | return; 36 | }, 37 | }; 38 | 39 | type LogLevel = keyof typeof defaultHandlers; 40 | type LogFunction = (typeof defaultHandlers)[T]; 41 | 42 | class Logger { 43 | handlers = defaultHandlers; 44 | private static currentLevel = LogLevels.ERROR; 45 | 46 | get level(): LogLevels { 47 | return Logger.currentLevel; 48 | } 49 | 50 | setLevel(level: LogLevels): void { 51 | Logger.currentLevel = level; 52 | } 53 | 54 | setHandler(level: T, callback: LogFunction): void { 55 | this.handlers[level] = callback; 56 | } 57 | 58 | log(level: LogLevels.ERROR, message: string | Error): void; 59 | log(level: LogLevel, message: string): void; 60 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 61 | log(level: LogLevel, message: any): void { 62 | if (this.level >= level) this.handlers[level](message); 63 | } 64 | 65 | info(message: string) { 66 | this.log(LogLevels.INFO, message); 67 | } 68 | 69 | warning(message: string) { 70 | this.log(LogLevels.WARNING, message); 71 | } 72 | 73 | error(message: string | Error) { 74 | this.log(LogLevels.ERROR, message); 75 | } 76 | 77 | debug(message: string) { 78 | this.log(LogLevels.DEBUG, message); 79 | } 80 | } 81 | 82 | export default new Logger(); 83 | -------------------------------------------------------------------------------- /src/resources/2023/BurningLeaves.ts: -------------------------------------------------------------------------------- 1 | import { Item, Monster, cliExecute, itemAmount, runChoice } from "kolmafia"; 2 | import { haveInCampground } from "../../lib.js"; 3 | import { get } from "../../property.js"; 4 | import { $item, $monster } from "../../template-string.js"; 5 | 6 | const item = $item`A Guide to Burning Leaves`; 7 | 8 | export const burnFor = new Map([ 9 | [$monster`flaming leaflet`, 11], 10 | [$item`autumnic bomb`, 37], 11 | [$item`impromptu torch`, 42], 12 | [$item`flaming fig leaf`, 43], 13 | [$item`smoldering drape`, 44], 14 | [$item`distilled resin`, 50], 15 | [$item`autumnal aegis`, 66], 16 | [$item`lit leaf lasso`, 69], 17 | [$item`forest canopy bed`, 74], 18 | [$item`autumnic balm`, 99], 19 | [$monster`flaming monstera`, 111], 20 | [$item`day shortener`, 222], 21 | [$monster`leaviathan`, 666], 22 | [$item`coping juice`, 1111], 23 | [$item`smoldering leafcutter ant egg`, 6666], 24 | [$item`super-heated leaf`, 11111], 25 | ]); 26 | 27 | /** 28 | * @returns Whether or not we currently `have` the GuidetoBurningLeaves 29 | */ 30 | export function have(): boolean { 31 | return haveInCampground(item); 32 | } 33 | 34 | /** 35 | * @returns The number of leaves we have remaining 36 | */ 37 | export function numberOfLeaves(): number { 38 | return itemAmount($item`inflammable leaf`); 39 | } 40 | 41 | /** 42 | * @returns Whether or not we can do the requested burn 43 | * @param leaves determines the number of leaves to burn 44 | */ 45 | export function burnSpecialLeaves(leaves: Item | Monster): boolean { 46 | const lea = burnFor.get(leaves); 47 | if (lea === undefined || lea > numberOfLeaves()) { 48 | return false; 49 | } 50 | return cliExecute(`leaves ${leaves}`); 51 | } 52 | 53 | /** 54 | * @returns Whether or not we can do the requested burn 55 | * @param leaves determines the number of leaves to burn 56 | */ 57 | export function burnLeaves(leaves: number): boolean { 58 | if (leaves > numberOfLeaves()) { 59 | return false; 60 | } 61 | return cliExecute(`leaves ${leaves}`); 62 | } 63 | 64 | function visitLeaves() { 65 | cliExecute("leaves"); 66 | } 67 | 68 | /** 69 | * Checks whether you can, then Jumps in the Flames 70 | * @returns Whether or not you jumped in the flames 71 | */ 72 | export function jumpInFire(): boolean { 73 | if (get("_leavesJumped")) { 74 | return false; 75 | } 76 | if (get("_leavesBurned") === 0) { 77 | return false; 78 | } 79 | visitLeaves(); 80 | runChoice(2); 81 | return get("_leavesJumped"); 82 | } 83 | -------------------------------------------------------------------------------- /src/resources/2019/CampAway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Effect, 3 | daycount, 4 | myPath, 5 | mySign, 6 | retrieveItem, 7 | use, 8 | visitUrl, 9 | } from "kolmafia"; 10 | import { signIdToName } from "../../moonSign.js"; 11 | import { get, withChoice } from "../../property.js"; 12 | import { $item } from "../../template-string.js"; 13 | import { random } from "../../utils.js"; 14 | 15 | /** 16 | * Determines whether we `have` the campsite 17 | * 18 | * @returns Whether we `have` the campsite 19 | */ 20 | export function have(): boolean { 21 | return get("getawayCampsiteUnlocked"); 22 | } 23 | 24 | /** 25 | * @returns Number of cloud buffs acquired today 26 | */ 27 | export function getCloudBuffsToday(): number { 28 | return get("_campAwayCloudBuffs"); 29 | } 30 | 31 | /** 32 | * @returns Number of cloud buffs acquired today 33 | */ 34 | export function getSmileBuffsToday(): number { 35 | return get("_campAwaySmileBuffs"); 36 | } 37 | 38 | /** 39 | * @returns Number of buffs acquired today from gazing at the stars 40 | */ 41 | export function getBuffsToday(): number { 42 | return getCloudBuffsToday() + getSmileBuffsToday(); 43 | } 44 | 45 | /** 46 | * @returns Whether player has acquired all their buffs today from gazing at the stars 47 | */ 48 | export function canGaze(): boolean { 49 | return getBuffsToday() < 4; 50 | } 51 | 52 | /** 53 | * Gaze at the stars 54 | */ 55 | export function gaze(): void { 56 | if (!canGaze()) return; 57 | visitUrl("place.php?whichplace=campaway&action=campaway_sky"); 58 | } 59 | 60 | /** 61 | * @param daycountToCheck Daycount to check, defaults to today 62 | * @returns The buff that the user will get if they gaze on the supplied daycount 63 | */ 64 | export function getGazeBuff(daycountToCheck = daycount()): Effect { 65 | const buffSign = signIdToName(((daycountToCheck + myPath().id) % 9) + 1); 66 | 67 | const effectName = []; 68 | 69 | if (buffSign === mySign()) effectName.push("Big"); 70 | effectName.push("Smile of the", buffSign); 71 | 72 | return Effect.get(effectName.join(" ")); 73 | } 74 | 75 | /** 76 | * Retrieve a number of campfire smokes and use them selecting randomly from the supplied messages 77 | * 78 | * @param messages Array of messages to blow 79 | * @param times Number of times to blow smoke 80 | */ 81 | export function blowSmoke(messages: string[], times = 1): void { 82 | const smoke = $item`campfire smoke`; 83 | retrieveItem(smoke, times); 84 | 85 | for (let i = 0; i < times; i++) { 86 | withChoice(1394, `1&message=${random(messages)}`, () => { 87 | use(smoke); 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tools/parseItemSkillNames.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "fs/promises"; 2 | import path from "path"; 3 | import url from "url"; 4 | import nodeFetch from "node-fetch"; 5 | 6 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); 7 | 8 | const SKILLS_FILE = 9 | "https://raw.githubusercontent.com/kolmafia/kolmafia/main/src/data/classskills.txt"; 10 | const ITEMS_FILE = 11 | "https://raw.githubusercontent.com/kolmafia/kolmafia/main/src/data/items.txt"; 12 | 13 | const OVERLAPPING_NAMES_FILE = path.join( 14 | __dirname, 15 | "../src/overlappingNames.ts", 16 | ); 17 | 18 | async function overlappingItems(): Promise { 19 | const response = await nodeFetch(ITEMS_FILE); 20 | const text = await response.text(); 21 | const lines = text.split("\n"); 22 | 23 | const items: string[] = []; 24 | for (const line of lines) { 25 | if (!line || line.startsWith("#") || !line.includes("\t")) continue; 26 | const [_id, name, _descid, _image, use, _access, _autosell, _discard] = 27 | line.split("\t"); 28 | if (use.includes("combat")) items.push(name); 29 | } 30 | 31 | return items.filter((name1) => 32 | items.some( 33 | (name2) => 34 | name2 !== name1 && name2.toLowerCase().includes(name1.toLowerCase()), 35 | ), 36 | ); 37 | } 38 | async function overlappingSkills(): Promise { 39 | const response = await nodeFetch(SKILLS_FILE); 40 | const text = await response.text(); 41 | const lines = text.split("\n"); 42 | 43 | const skills: string[] = []; 44 | for (const line of lines) { 45 | if (!line || line.startsWith("#") || !line.includes("\t")) continue; 46 | const [_id, name, _image, tags, _mpCost] = line.split("\t"); 47 | if (tags.split(",").includes("combat")) skills.push(name); 48 | } 49 | 50 | return skills.filter((name1) => 51 | skills.some( 52 | (name2) => 53 | name2 !== name1 && name2.toLowerCase().includes(name1.toLowerCase()), 54 | ), 55 | ); 56 | } 57 | 58 | async function main() { 59 | const items = await overlappingItems(); 60 | const skills = await overlappingSkills(); 61 | 62 | let contents = `/** THIS FILE IS AUTOMATICALLY GENERATED. See tools/parseItemSkillNames.ts for more information */\n\n`; 63 | 64 | contents += "export const overlappingItemNames = [\n"; 65 | contents += items.map((item) => ` "${item}",\n`).join(""); 66 | contents += "];\n\n"; 67 | 68 | contents += "export const overlappingSkillNames = [\n"; 69 | contents += skills.map((skill) => ` "${skill}",\n`).join(""); 70 | contents += "];\n"; 71 | 72 | await writeFile(OVERLAPPING_NAMES_FILE, contents); 73 | } 74 | 75 | main(); 76 | -------------------------------------------------------------------------------- /src/resources/2013/JungMan.ts: -------------------------------------------------------------------------------- 1 | import { haveFamiliar, visitUrl } from "kolmafia"; 2 | import { get } from "../../property.js"; 3 | import { $familiar, $item } from "../../template-string.js"; 4 | 5 | const familiar = $familiar`Angry Jung Man`; 6 | 7 | /** 8 | * @returns Whether we `have` the Jung Man 9 | */ 10 | export function have(): boolean { 11 | return haveFamiliar(familiar); 12 | } 13 | 14 | export enum Jar { 15 | SUSPICIOUS_GUY = "susguy", 16 | GOURD_CAPTAIN = "gourdcaptain", 17 | CRACKPOT_MYSTIC = "mystic", 18 | OLD_MAN = "oldman", 19 | PRETENTIOUS_ARTIST = "artist", 20 | MEATSMITH = "meatsmith", 21 | JICK = "jick", 22 | } 23 | 24 | const PLACES = { 25 | [Jar.PRETENTIOUS_ARTIST]: ["place", "town_wrong"], 26 | [Jar.GOURD_CAPTAIN]: ["place", "town_right"], 27 | [Jar.CRACKPOT_MYSTIC]: ["shop", "mystic"], 28 | [Jar.OLD_MAN]: ["place", "sea_oldman"], 29 | [Jar.MEATSMITH]: ["shop", "meatsmith"], 30 | [Jar.JICK]: ["showplayer", "1"], 31 | [Jar.SUSPICIOUS_GUY]: ["tavern"], 32 | } as const; 33 | 34 | export const JAR_ITEMS = { 35 | [Jar.SUSPICIOUS_GUY]: $item`jar of psychoses (The Suspicious-Looking Guy)`, 36 | [Jar.GOURD_CAPTAIN]: $item`jar of psychoses (The Captain of the Gourd)`, 37 | [Jar.CRACKPOT_MYSTIC]: $item`jar of psychoses (The Crackpot Mystic)`, 38 | [Jar.OLD_MAN]: $item`jar of psychoses (The Old Man)`, 39 | [Jar.PRETENTIOUS_ARTIST]: $item`jar of psychoses (The Pretentious Artist)`, 40 | [Jar.MEATSMITH]: $item`jar of psychoses (The Meatsmith)`, 41 | [Jar.JICK]: $item`jar of psychoses (Jick)`, 42 | }; 43 | 44 | function getJungUrl(jar: Jar) { 45 | const [page, answer] = PLACES[jar]; 46 | const question = page === "showplayer" ? "who" : `which${page}`; 47 | const params = [ 48 | ["action", "jung"], 49 | ["whichperson", jar], 50 | ]; 51 | if (answer) params.push([question, answer]); 52 | return `${page}.php?${params.map((pair) => pair.join("=")).join("&")}`; 53 | } 54 | 55 | /** 56 | * @returns Whether we can currently make a Jick jar 57 | */ 58 | export function canJickJar(): boolean { 59 | if (get("_jickJarAvailable") === "unknown") visitUrl("showplayer.php?who=1"); 60 | return get("_jickJarAvailable") === "true" && !get("_psychoJarFilled"); 61 | } 62 | 63 | /** 64 | * Tries to make a psychoanalytic jar with the chosen target 65 | * 66 | * @param jar The character of Loathing to psychoanalyze 67 | * @returns Whether we successfully crafted the jar 68 | */ 69 | export function makeJar(jar: Jar): boolean { 70 | if (jar === Jar.JICK && !canJickJar()) return false; 71 | const result = visitUrl(getJungUrl(jar)); 72 | return result.includes( 73 | "You open up the jar and look into the patient's eyes.", 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/Kmail.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import Kmail from "./Kmail.js"; 3 | import { Item } from "kolmafia"; 4 | 5 | describe("Kmail parsing", () => { 6 | it("skips a Valentine", () => { 7 | const kmail = Kmail.parse({ 8 | id: "1", 9 | azunixtime: "1744557183000", 10 | localtime: "1744557183000", 11 | type: "normal", 12 | fromid: "1829137", 13 | fromname: "Seraphiii", 14 | message: 15 | "
You zerg rush'd my heart
Your love gets me high
Come give me a kiss
You're oh em gee KAWAIIIIIIII!!11!!11!!!?!!?!
", 16 | }); 17 | 18 | expect(kmail.message).toBe(""); 19 | }); 20 | 21 | it("parses a kmail with items and a message", () => { 22 | const kmail = Kmail.parse({ 23 | id: "182501176", 24 | azunixtime: "1744557183000", 25 | localtime: "1744557183000", 26 | type: "normal", 27 | fromid: "1829137", 28 | fromname: "Seraphiii", 29 | message: 30 | 'Enjoy!
toastYou acquire an item: toast
', 31 | }); 32 | 33 | expect(kmail.message).toBe("Enjoy!"); 34 | 35 | const items = kmail.items(); 36 | expect(items.size).toBe(1); 37 | expect(items.has(Item.get("toast"))).toBe(true); 38 | expect(items.get(Item.get("toast"))).toBe(1); 39 | }); 40 | 41 | it("parses a kmail with items but no message", () => { 42 | const kmail = Kmail.parse({ 43 | id: "182501176", 44 | azunixtime: "1744557183000", 45 | localtime: "1744557183000", 46 | type: "normal", 47 | fromid: "1829137", 48 | fromname: "Seraphiii", 49 | message: 50 | '
pat of butterYou acquire 2 pats of butter
', 51 | }); 52 | 53 | expect(kmail.message).toBe(""); 54 | 55 | const items = kmail.items(); 56 | expect(items.size).toBe(1); 57 | expect(items.has(Item.get("pats of butter"))).toBe(true); 58 | expect(items.get(Item.get("pats of butter"))).toBe(2); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libram", 3 | "version": "0.11.14", 4 | "description": "JavaScript helper library for KoLmafia", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "repository": "https://github.com/loathers/libram", 9 | "author": "Samuel Gaus ", 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "yarn run updateOverlappingItemSkillNames && yarn run build:tsc && yarn run build:bundled", 13 | "build:tsc": "tsc", 14 | "build:bundled": "node build.mjs", 15 | "clean": "rm -rf dist", 16 | "docs": "yarn run typedoc", 17 | "format": "yarn run prettier --write .", 18 | "lint": "yarn run eslint && yarn run prettier --check .", 19 | "test": "yarn run vitest", 20 | "prepack": "yarn run build && pinst --disable", 21 | "updateProps": "node --import tsx ./tools/parseDefaultProperties.ts", 22 | "updateOverlappingItemSkillNames": "node --import tsx ./tools/parseItemSkillNames.ts", 23 | "parseModifiers": "node --import tsx ./tools/parseModifiers.ts", 24 | "postinstall": "husky install", 25 | "postpack": "pinst --enable" 26 | }, 27 | "files": [ 28 | "dist/**/*.js", 29 | "dist/**/*.d.ts" 30 | ], 31 | "devDependencies": { 32 | "@babel/compat-data": "^7.25.4", 33 | "@babel/core": "^7.25.2", 34 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 35 | "@babel/plugin-transform-runtime": "^7.25.4", 36 | "@babel/preset-env": "^7.25.4", 37 | "@babel/preset-typescript": "^7.24.7", 38 | "@eslint/js": "^9.39.2", 39 | "@tsconfig/node20": "^20.1.4", 40 | "@types/lodash-es": "^4.17.12", 41 | "@types/node": "^22.5.4", 42 | "esbuild": "^0.23.1", 43 | "esbuild-plugin-babel": "^0.2.3", 44 | "eslint": "^9.39.2", 45 | "eslint-config-prettier": "^10.1.8", 46 | "eslint-import-resolver-typescript": "^4.4.4", 47 | "eslint-plugin-jsdoc": "^61.5.0", 48 | "eslint-plugin-libram": "^0.5.3", 49 | "husky": "^9.1.6", 50 | "java-parser": "^2.3.2", 51 | "kolmafia": "^5.28549.0", 52 | "lint-staged": "^15.2.10", 53 | "lodash-es": "^4.17.21", 54 | "node-fetch": "^3.3.2", 55 | "pinst": "^3.0.0", 56 | "prettier": "^3.3.3", 57 | "tsx": "^4.19.1", 58 | "typedoc": "^0.26.7", 59 | "typescript": "^5.6.2", 60 | "typescript-eslint": "^8.50.0", 61 | "vitest": "^1.6.0", 62 | "vitest-github-actions-reporter": "^0.11.1" 63 | }, 64 | "peerDependencies": { 65 | "kolmafia": "^5.28100.0" 66 | }, 67 | "lint-staged": { 68 | "src/**/*.{ts,js}": "yarn run format" 69 | }, 70 | "sideEffects": false, 71 | "packageManager": "yarn@4.4.1", 72 | "dependencies": { 73 | "core-js": "^3.47.0", 74 | "entities": "^7.0.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/diet/knapsack.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { knapsack } from "./knapsack.js"; 3 | 4 | function sortValues(items: [T, number][]) { 5 | return items.sort(([x], [y]) => (x < y ? -1 : x === y ? 0 : 1)); 6 | } 7 | 8 | describe("knapsack", () => { 9 | it("selects better items", () => { 10 | const [value, items] = knapsack( 11 | [ 12 | ["A", 1, 1], 13 | ["B", 3, 2], 14 | ], 15 | 4, 16 | ); 17 | 18 | expect(sortValues(items)).toEqual([["B", 2]]); 19 | expect(value).toEqual(6); 20 | }); 21 | 22 | it("beats greedy", () => { 23 | // Greedy would select CCC then A (25), but CCB (26) is better. 24 | const [value, items] = knapsack( 25 | [ 26 | ["A", 1, 1], 27 | ["B", 10, 3], 28 | ["C", 8, 2], 29 | ], 30 | 7, 31 | ); 32 | 33 | expect(sortValues(items)).toEqual([ 34 | ["B", 1], 35 | ["C", 2], 36 | ]); 37 | expect(value).toEqual(26); 38 | }); 39 | 40 | it("doesn't overfill", () => { 41 | const [value, items] = knapsack( 42 | [ 43 | ["A", 2, 2], 44 | ["B", 2, 4], 45 | ], 46 | 5, 47 | ); 48 | 49 | expect(sortValues(items)).toEqual([["A", 2]]); 50 | expect(value).toEqual(4); 51 | }); 52 | 53 | it("respects maximum quantity", () => { 54 | const [value, items] = knapsack( 55 | [ 56 | ["A", 10, 1, 1], 57 | ["B", 1, 2], 58 | ], 59 | 5, 60 | ); 61 | 62 | expect(sortValues(items)).toEqual([ 63 | ["A", 1], 64 | ["B", 2], 65 | ]); 66 | expect(value).toEqual(12); 67 | }); 68 | 69 | it("respects zero maximum quantity", () => { 70 | const [value, items] = knapsack( 71 | [ 72 | ["A", 10, 1, 0], 73 | ["B", 1, 2], 74 | ], 75 | 4, 76 | ); 77 | 78 | expect(sortValues(items)).toEqual([["B", 2]]); 79 | expect(value).toEqual(2); 80 | }); 81 | 82 | it("uses negative items", () => { 83 | const [value, items] = knapsack( 84 | [ 85 | ["A", 1, 1], 86 | ["B", 0, -1, 1], 87 | ], 88 | 2, 89 | ); 90 | 91 | expect(sortValues(items)).toEqual([ 92 | ["A", 3], 93 | ["B", 1], 94 | ]); 95 | expect(value).toEqual(3); 96 | }); 97 | 98 | it("uses negative items with cost", () => { 99 | const [value, items] = knapsack( 100 | [ 101 | ["A", 2, 1], 102 | ["B", -1, -1, 1], 103 | ], 104 | 2, 105 | ); 106 | 107 | expect(sortValues(items)).toEqual([ 108 | ["A", 3], 109 | ["B", 1], 110 | ]); 111 | expect(value).toEqual(5); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/resources/2025/PeridotOfPeril.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getMonsters, 3 | Location, 4 | Monster, 5 | setProperty, 6 | toLocation, 7 | } from "kolmafia"; 8 | import { have as have_ } from "../../lib.js"; 9 | import { get, Properties } from "../../property.js"; 10 | import { $item } from "../../template-string.js"; 11 | 12 | const peridot = $item`Peridot of Peril`; 13 | 14 | /** 15 | * @returns Whether you `have` the Peridot of Peril. 16 | */ 17 | export function have(): boolean { 18 | return have_(peridot); 19 | } 20 | 21 | /** 22 | * @returns An array of what locations you've used your peridot of peril in today. 23 | */ 24 | export function zonesToday(): Location[] { 25 | return get("_perilLocations") 26 | .split(",") 27 | .filter(Boolean) 28 | .map((id) => toLocation(Number(id))); 29 | } 30 | 31 | /** 32 | * Determine if you've already used your peridot NC in a particular location today. 33 | * @param location The location in question. 34 | * 35 | * @returns Whether or not you've used the peridot in that location yet today. 36 | */ 37 | export function periledToday(location: Location): boolean { 38 | return RegExp(`(?:^|,)${location.id}(?:$|,)`).test(get("_perilLocations")); 39 | } 40 | 41 | /** 42 | * Determine if you can get the peridot NC in a particular location. 43 | * @param location The location in question. 44 | * 45 | * @returns Whether or not you can currently get the peridot NC in that location. 46 | */ 47 | export function canImperil(location: Location): boolean { 48 | return ( 49 | have() && 50 | location.wanderers && 51 | location.combatPercent >= 0 && 52 | getMonsters(location).length > 0 && 53 | !periledToday(location) 54 | ); 55 | } 56 | 57 | const toChoiceOption = (m: Monster) => `1&bandersnatch=${m.id}`; 58 | 59 | /** 60 | * Create a `Properties` object for handling the peridot choice for a given monster. 61 | * @param monster The monster in question. 62 | * 63 | * @returns A `Properties` object to handle the peridot choice for the given monster. 64 | */ 65 | export function getChoiceProperty(monster: Monster): Properties { 66 | return { choiceAdventure1557: toChoiceOption(monster) }; 67 | } 68 | 69 | /** 70 | * Create a choices object keyed by choice id, instead of property name. 71 | * @param monster The monster in question. 72 | * @returns An object keyed by choice id, with values of choice options. 73 | */ 74 | export function getChoiceObject(monster: Monster): Record { 75 | return { 1557: toChoiceOption(monster) }; 76 | } 77 | /** 78 | * Set the `choiceAdventure1557` pref to fight the given monster. 79 | * @param monster The monster in question. 80 | */ 81 | export function setChoice(monster: Monster) { 82 | setProperty("choiceAdventure1557", toChoiceOption(monster)); 83 | } 84 | -------------------------------------------------------------------------------- /src/url.ts: -------------------------------------------------------------------------------- 1 | import { visitUrl } from "kolmafia"; 2 | 3 | export const EMPTY_VALUE = Symbol("empty"); 4 | type QValue = string | number | boolean | typeof EMPTY_VALUE; 5 | export type QueryList = readonly (readonly [key: string, value: QValue])[]; 6 | export type QueryObject = Record; 7 | export type Query = QueryList | QueryObject; 8 | 9 | type Options = { 10 | method?: "GET" | "POST"; 11 | }; 12 | 13 | /** 14 | * Fetches a URL and returns the response 15 | * @param path Path to resource, e.g. "clan_basement.php" 16 | * @param query Query parameters, 17 | * either as an object, e.g. { action: "cleansewer" }, 18 | * or as a list of [key, value] pairs, e.g. [["action", "cleansewer"]] 19 | * @param options Additional options 20 | * @param options.method HTTP method to use, either "GET" or "POST", defaults to "POST" 21 | * @returns the response from visiting the URL 22 | */ 23 | export function fetchUrl( 24 | path: string, 25 | query: Query = [], 26 | options: Options = {}, 27 | ): string { 28 | const { method = "POST" } = options; 29 | 30 | const url = buildUrl(path, query); 31 | 32 | return visitUrl(url, method === "POST", true); 33 | } 34 | 35 | /** 36 | * Builds a URL from a path and query 37 | * @param path Path to resource, e.g. "clan_basement.php" 38 | * @param query Query parameters, 39 | * either as an object, e.g. { action: "cleansewer" }, 40 | * or as a list of [key, value] pairs, e.g. [["action", "cleansewer"]] 41 | * @returns the constructed URL, e.g. "clan_basement.php?action=cleansewer" 42 | */ 43 | export function buildUrl(path: string, query: Query = []): string { 44 | const urlParams = Array.isArray(query) ? query : Object.entries(query); 45 | if (urlParams.length === 0) { 46 | return path; 47 | } 48 | 49 | const chunks = [path]; 50 | let sep = path.includes("?") ? "&" : "?"; 51 | for (const param of urlParams) { 52 | if (param.length !== 2) { 53 | throw new Error(`Query parameter array may only contain pair elements`); 54 | } 55 | const [key, value] = param; 56 | chunks.push(sep); 57 | sep = "&"; 58 | chunks.push(encodeURIComponent(key)); 59 | if (value !== EMPTY_VALUE) { 60 | chunks.push("="); 61 | chunks.push(encodeURIComponent(value)); 62 | } 63 | } 64 | return chunks.join(""); 65 | } 66 | 67 | /** 68 | * Combines a list of queries into a single query 69 | * @param queries a list of query objects and/or arrays, can be mixed 70 | * @returns a single query 71 | */ 72 | export function combineQuery(...queries: Query[]): Query { 73 | if (queries.length === 1) { 74 | return queries[0]; 75 | } 76 | const result = []; 77 | for (const query of queries) { 78 | if (Array.isArray(query)) { 79 | result.push(...query); 80 | } else { 81 | result.push(...Object.entries(query)); 82 | } 83 | } 84 | return result; 85 | } 86 | -------------------------------------------------------------------------------- /src/resources/2019/Snapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cliExecute, 3 | Familiar, 4 | haveFamiliar, 5 | Item, 6 | myFamiliar, 7 | Phylum, 8 | useFamiliar, 9 | } from "kolmafia"; 10 | import { get } from "../../property.js"; 11 | 12 | const familiar = Familiar.get("Red-Nosed Snapper"); 13 | 14 | /** 15 | * Map of phylum to item that phylum drops. 16 | */ 17 | export const phylumItem = new Map([ 18 | [Phylum.get("beast"), Item.get("patch of extra-warm fur")], 19 | [Phylum.get("bug"), Item.get("a bug's lymph")], 20 | [Phylum.get("constellation"), Item.get("micronova")], 21 | [Phylum.get("construct"), Item.get("industrial lubricant")], 22 | [Phylum.get("demon"), Item.get("infernal snowball")], 23 | [Phylum.get("dude"), Item.get("human musk")], 24 | [Phylum.get("elemental"), Item.get("livid energy")], 25 | [Phylum.get("elf"), Item.get("peppermint syrup")], 26 | [Phylum.get("fish"), Item.get("fish sauce")], 27 | [Phylum.get("goblin"), Item.get("guffin")], 28 | [Phylum.get("hippy"), Item.get("organic potpourri")], 29 | [Phylum.get("hobo"), Item.get("beggin' cologne")], 30 | [Phylum.get("horror"), Item.get("powdered madness")], 31 | [Phylum.get("humanoid"), Item.get("vial of humanoid growth hormone")], 32 | [Phylum.get("mer-kin"), Item.get("Mer-kin eyedrops")], 33 | [Phylum.get("orc"), Item.get("boot flask")], 34 | [Phylum.get("penguin"), Item.get("envelope full of Meat")], 35 | [Phylum.get("pirate"), Item.get("Shantix™")], 36 | [Phylum.get("plant"), Item.get("goodberry")], 37 | [Phylum.get("slime"), Item.get("extra-strength goo")], 38 | [Phylum.get("undead"), Item.get("unfinished pleasure")], 39 | [Phylum.get("weird"), Item.get("non-Euclidean angle")], 40 | ]); 41 | 42 | /** 43 | * Map of drop item to phylum it drops from. 44 | */ 45 | export const itemPhylum = new Map( 46 | [...phylumItem].map(([phylum, item]) => [item, phylum]), 47 | ); 48 | 49 | /** 50 | * Return whether you have a Red-Nosed Snapper. 51 | * 52 | * @returns True if you have a Red-Nosed Snapper, false otherwise. 53 | */ 54 | export function have(): boolean { 55 | return haveFamiliar(familiar); 56 | } 57 | 58 | /** 59 | * Get the phylum currently being tracked by the snapper. 60 | * 61 | * @returns Tracked phylum, or null if no phylum tracked. 62 | */ 63 | export function getTrackedPhylum(): Phylum | null { 64 | return get("redSnapperPhylum"); 65 | } 66 | 67 | /** 68 | * Set snapper tracking to a certain phylum. 69 | * 70 | * @param phylum Phylum to track. 71 | */ 72 | export function trackPhylum(phylum: Phylum): void { 73 | const currentFamiliar = myFamiliar(); 74 | try { 75 | useFamiliar(familiar); 76 | cliExecute(`snapper ${phylum}`); 77 | } finally { 78 | useFamiliar(currentFamiliar); 79 | } 80 | } 81 | 82 | /** 83 | * Get progress to next snapper drop. 84 | * 85 | * @returns Number of fights completed (out of 11) to reach next drop. 86 | */ 87 | export function getProgress(): number { 88 | return get("redSnapperProgress"); 89 | } 90 | -------------------------------------------------------------------------------- /src/resources/2016/GingerBread.ts: -------------------------------------------------------------------------------- 1 | import { Location, canAdventure } from "kolmafia"; 2 | import { get } from "../../property.js"; 3 | import { $location, $locations } from "../../template-string.js"; 4 | 5 | /** 6 | * @returns Whether or not you can currently access Gingerbread City 7 | */ 8 | export function available(): boolean { 9 | return ( 10 | (get("gingerbreadCityAvailable") || get("_gingerbreadCityToday")) && 11 | turns() < availableTurns() 12 | ); 13 | } 14 | 15 | function turns(): number { 16 | return ( 17 | get("_gingerbreadCityTurns") + (get("_gingerbreadClockAdvanced") ? 5 : 0) 18 | ); 19 | } 20 | 21 | function availableTurns(): number { 22 | return 20 + (get("gingerExtraAdventures") ? 10 : 0); 23 | } 24 | 25 | /** 26 | * @returns The number of Gingerbread encounters until it's Midnight in the city; this may be negative if the time has passed 27 | */ 28 | export function minutesToMidnight(): number { 29 | return 19 - turns(); 30 | } 31 | 32 | /** 33 | * @returns The number of Gingerbread encounters until it's Noon in the city; this may be negative if the time has passed 34 | */ 35 | export function minutesToNoon(): number { 36 | return 9 - turns(); 37 | } 38 | 39 | export const LOCATIONS = Object.freeze( 40 | $locations`Gingerbread Civic Center, Gingerbread Train Station, Gingerbread Industrial Zone, Gingerbread Upscale Retail District, Gingerbread Sewers`, 41 | ); 42 | 43 | /** 44 | * @returns A list of all Gingerbread locations at which you can currently adventure 45 | */ 46 | export function availableLocations(): Location[] { 47 | return LOCATIONS.filter((l) => canAdventure(l)); 48 | } 49 | 50 | const NOONS = new Map([ 51 | [$location`Gingerbread Train Station`, 1204], 52 | [$location`Gingerbread Civic Center`, 1202], 53 | [$location`Gingerbread Industrial Zone`, 1206], 54 | [$location`Gingerbread Upscale Retail District`, 1208], 55 | ]); 56 | 57 | const MIDNIGHTS = new Map([ 58 | [$location`Gingerbread Train Station`, 1205], 59 | [$location`Gingerbread Civic Center`, 1203], 60 | [$location`Gingerbread Industrial Zone`, 1207], 61 | [$location`Gingerbread Upscale Retail District`, 1209], 62 | ]); 63 | 64 | /** 65 | * @param location The location in question 66 | * @returns The id of the Noon choice adventure at that location; 0 if inapplicable 67 | */ 68 | export function getNoonChoiceId(location: Location): number { 69 | return NOONS.get(location) ?? 0; 70 | } 71 | 72 | /** 73 | * @param location The location in question 74 | * @returns The id of the Midnight choice adventure at that location; 0 if inapplicable 75 | */ 76 | export function getMidnightChoiceId(location: Location): number { 77 | return MIDNIGHTS.get(location) ?? 0; 78 | } 79 | 80 | /** 81 | * @returns Whether or not it is possible for you to fight Judge Fudge today 82 | */ 83 | export function canJudgeFudge(): boolean { 84 | if (minutesToNoon() >= 0) { 85 | return true; 86 | } 87 | if (minutesToMidnight() >= 0 && get("_gingerbreadColumnDestroyed")) { 88 | return true; 89 | } 90 | return false; 91 | } 92 | -------------------------------------------------------------------------------- /src/resources/2024/EverfullDarts.ts: -------------------------------------------------------------------------------- 1 | import { have as have_ } from "../../lib.js"; 2 | import { $item } from "../../template-string.js"; 3 | import { get } from "../../property.js"; 4 | import { clamp } from "../../utils.js"; 5 | 6 | const item = $item`Everfull Dart Holster`; 7 | 8 | /** 9 | * @returns whether you `have` the Everfull Dart Holster 10 | */ 11 | export function have(): boolean { 12 | return have_(item); 13 | } 14 | 15 | const PERKS = [ 16 | "Throw a second dart quickly", 17 | "Deal 25-50% more damage", 18 | "You are less impressed by bullseyes", 19 | "25% Better bullseye targeting", 20 | "Extra stats from stats targets", 21 | "Butt awareness", 22 | "Add Hot Damage", 23 | "Add Cold Damage", 24 | "Add Sleaze Damage", 25 | "Add Spooky Damage", 26 | "Add Stench Damage", 27 | "Expand your dart capacity by 1", 28 | "Bullseyes do not impress you much", 29 | "25% More Accurate bullseye targeting", 30 | "Deal 25-50% extra damage", 31 | "Expand your dart capacity by 1", 32 | "Increase Dart Deleveling from deleveling targets", 33 | "Deal 25-50% greater damage", 34 | "Extra stats from stats targets", 35 | "25% better chance to hit bullseyes", 36 | ] as const; 37 | export type Perk = (typeof PERKS)[number]; 38 | 39 | /** 40 | * @returns An array consisting of the current dart perks you have unlocked 41 | */ 42 | export function currentPerks(): Perk[] { 43 | return get("everfullDartPerks").split(",") as Perk[]; 44 | } 45 | 46 | /** 47 | * @returns Whether you currently have all possible dart perks 48 | */ 49 | export function perksMaxed(): boolean { 50 | return currentPerks().length === PERKS.length; 51 | } 52 | 53 | function makePerkFunction( 54 | perkOrPerks: readonly Perk[] | Perk, 55 | formula: (perks: number) => number, 56 | ) { 57 | const current = currentPerks(); 58 | if (Array.isArray(perkOrPerks)) { 59 | return () => 60 | formula(perkOrPerks.filter((perk) => current.includes(perk)).length); 61 | } 62 | return () => formula(current.filter((perk) => perk === perkOrPerks).length); 63 | } 64 | 65 | const BULLSEYE_ACCURACY_PERKS = [ 66 | "25% Better bullseye targeting", 67 | "25% better chance to hit bullseyes", 68 | "25% More Accurate bullseye targeting", 69 | ] as const; 70 | /** 71 | * @returns The chance of landing a bullseye 72 | */ 73 | export const bullseyeChance = makePerkFunction( 74 | BULLSEYE_ACCURACY_PERKS, 75 | (perks) => clamp(0.25 * (1 + perks), 0, 1), 76 | ); 77 | 78 | const BULLSEYE_COOLDOWN_PERKS = [ 79 | "You are less impressed by bullseyes", 80 | "Bullseyes do not impress you much", 81 | ] as const; 82 | /** 83 | * @returns The current number of turns of Everything Looks Red you'll receive on a bullseye 84 | */ 85 | export const bullseyeCooldown = makePerkFunction( 86 | BULLSEYE_COOLDOWN_PERKS, 87 | (perks) => clamp(50 - 10 * perks, 30, 50), 88 | ); 89 | 90 | /** 91 | * @returns The total number of darts you can have available based on your current perks 92 | */ 93 | export const dartCapacity = makePerkFunction( 94 | "Expand your dart capacity by 1", 95 | (perks) => 1 + perks, 96 | ); 97 | -------------------------------------------------------------------------------- /src/resources/2022/JuneCleaver.ts: -------------------------------------------------------------------------------- 1 | import { toItem, availableAmount } from "kolmafia"; 2 | import { get } from "../../property.js"; 3 | 4 | export const cleaver = toItem("June cleaver"); 5 | 6 | /** 7 | * Determines whether you currently `have` the June cleaver 8 | * 9 | * @returns Whether you currently `have` the June cleaver 10 | */ 11 | export function have(): boolean { 12 | return availableAmount(cleaver) > 0; 13 | } 14 | 15 | /** 16 | * Determines the number of cleaver-combats it takes to get a particular encounter number. 17 | * 18 | * @param encounters The ordinal value of the June cleaver encounter you're asking about 19 | * @returns The number of cleaver-combats it takes to get a particular encounter number--this is agnostic of your current fights. 20 | */ 21 | export function getInterval( 22 | encounters = get("_juneCleaverEncounters"), 23 | ): number { 24 | return [1, 6, 10, 12, 15, 20][encounters] ?? 30; 25 | } 26 | 27 | /** 28 | * Determines the number of cleaver-combats it takes to get a particular encounter number, when a skip is in the mix 29 | * 30 | * @param encounters The ordinal value of the June cleaver encounter you're asking about, assuming you've skipped 31 | * @returns The number of cleaver-combats it would take to get a particular encounter after skipping. 32 | */ 33 | export function getSkippedInterval( 34 | encounters = get("_juneCleaverEncounters"), 35 | ): number { 36 | return [1, 2, 3, 3, 4, 5][encounters] ?? 8; 37 | } 38 | 39 | /** 40 | * Determines the amount of bonus elemental damage your cleaver currently grants 41 | * 42 | * @param element The element in question 43 | * @returns The bonus damage your cleaver currently gives for a given element. 44 | */ 45 | export function damage( 46 | element: "Hot" | "Stench" | "Sleaze" | "Spooky" | "Cold", 47 | ): number { 48 | return get(`_juneCleaver${element}`); 49 | } 50 | 51 | /** 52 | * Determines the number of times today you can skip a june cleaver choice 53 | * 54 | * @returns The number of additional times you can select option 4 in a cleaver choice today. 55 | */ 56 | export function skipsRemaining(): number { 57 | return 5 - get("_juneCleaverSkips"); 58 | } 59 | 60 | export const choices = [ 61 | 1467, 1468, 1469, 1470, 1471, 1472, 1473, 1474, 1475, 62 | ] as const; 63 | 64 | /** 65 | * Returns the current June cleaver queue; you are not currently able to encounter any June cleaver choice adventure in this list 66 | * 67 | * @returns An array consisting of the cleaver choice adventures currently in the queue. 68 | */ 69 | export function queue(): (typeof choices)[number][] { 70 | return get("juneCleaverQueue") 71 | .split(",") 72 | .filter((x) => x.trim().length > 0) 73 | .map((x) => parseInt(x)) as (typeof choices)[number][]; 74 | } 75 | 76 | /** 77 | * Determines which choices are currently eligible to be encountered with your June cleaver 78 | * 79 | * @returns An array consisting of the cleaver choice adventures not currently in the queue. 80 | */ 81 | export function choicesAvailable(): (typeof choices)[number][] { 82 | const currentQueue = queue(); 83 | return choices.filter((choice) => !currentQueue.includes(choice)); 84 | } 85 | -------------------------------------------------------------------------------- /src/resources/2008/Stickers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | availableAmount, 3 | equippedItem, 4 | haveSkill, 5 | Item, 6 | retrieveItem, 7 | visitUrl, 8 | } from "kolmafia"; 9 | import { $item, $items, $skill, $slots } from "../../template-string.js"; 10 | 11 | export const stickers = { 12 | unicorn: $item`scratch 'n' sniff unicorn sticker`, 13 | apple: $item`scratch 'n' sniff apple sticker`, 14 | UPC: $item`scratch 'n' sniff UPC sticker`, 15 | wrestler: $item`scratch 'n' sniff wrestler sticker`, 16 | dragon: $item`scratch 'n' sniff dragon sticker`, 17 | rockband: $item`scratch 'n' sniff rock band sticker`, 18 | } as const; 19 | 20 | export type Sticker = keyof typeof stickers; 21 | 22 | /** 23 | * @returns Whether the player has the Summon Stickers skill 24 | */ 25 | export function have(): boolean { 26 | return haveSkill($skill`Summon Stickers`); 27 | } 28 | 29 | /** 30 | * @returns The player's current sticker weapon 31 | */ 32 | export function weapon(): Item | null { 33 | return ( 34 | $items`scratch 'n' sniff sword, scratch 'n' sniff crossbow`.find((i) => 35 | availableAmount(i), 36 | ) ?? null 37 | ); 38 | } 39 | 40 | const weapons = { 41 | sword: $item`scratch 'n' sniff sword`, 42 | crossbow: $item`scratch 'n' sniff crossbow`, 43 | }; 44 | 45 | /** 46 | * Make a sword 47 | * 48 | * @param sticker Sticker to use when making the sword 49 | */ 50 | export function makeSword(sticker: Sticker): void { 51 | if (weapon()) return; 52 | visitUrl(`bedazzle.php?action=juststick&sticker=${stickers[sticker].id}&pwd`); 53 | } 54 | 55 | /** 56 | * Change weapon mode 57 | * 58 | * @param mode New weapon mode 59 | * @returns Success 60 | */ 61 | export function foldWeapon(mode: keyof typeof weapons): boolean { 62 | const currentWep = weapon(); 63 | if (!currentWep) return false; 64 | if (weapons[mode] === currentWep) return true; 65 | visitUrl("bedazzle.php?action=fold&pwd"); 66 | return weapons[mode] === currentWep; 67 | } 68 | 69 | /** 70 | * Get current stickers on sticker weapon 71 | * 72 | * @returns Tuple of stickers 73 | */ 74 | export function currentStickers(): [Item, Item, Item] { 75 | return $slots`sticker1, sticker2, sticker3`.map((s) => equippedItem(s)) as [ 76 | Item, 77 | Item, 78 | Item, 79 | ]; 80 | } 81 | 82 | /** 83 | * Set configuration for sticker weapon 84 | * 85 | * @param options Tuple of either sticker or null 86 | * @returns Resultant configuration 87 | */ 88 | export function setStickers( 89 | ...options: [Sticker | null, Sticker | null, Sticker | null] 90 | ): [Item, Item, Item] { 91 | for (const s of options) { 92 | if (s) retrieveItem(stickers[s], options.filter((x) => x === s).length); 93 | } 94 | 95 | visitUrl("bedazzle.php"); 96 | const start = currentStickers(); 97 | 98 | for (let i = 0; i <= 2; i++) { 99 | const sticker = options[i]; 100 | if (!sticker) continue; 101 | const item = stickers[sticker]; 102 | if (start[i] === item) continue; 103 | visitUrl(`bedazzle.php?action=peel&slot=${i + 1}&pwd`); 104 | visitUrl(`bedazzle.php?action=stick&slot=${i + 1}&sticker=${item.id}&pwd`); 105 | } 106 | 107 | return currentStickers(); 108 | } 109 | -------------------------------------------------------------------------------- /src/challengePaths/2014/HeavyRains.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Location, 3 | Modifier, 4 | Monster, 5 | monsterFactoidsAvailable, 6 | myRain, 7 | numericModifier, 8 | useSkill, 9 | visitUrl, 10 | } from "kolmafia"; 11 | import { have } from "../../lib.js"; 12 | import { withChoice } from "../../property.js"; 13 | import { $monster, $path, $skill } from "../../template-string.js"; 14 | 15 | /** 16 | * Cast Rain Man and fight the target monster 17 | * @param target the monster to fight 18 | */ 19 | export function rainMan(target: Monster): void { 20 | if (canRainMan(target)) { 21 | withChoice(970, `1&whichmonster=${target.id}`, () => 22 | useSkill($skill`Rain Man`), 23 | ); 24 | } 25 | } 26 | 27 | /** 28 | * Check if you can summon this monster using Rain Man 29 | * @param target the monster to attempt to summon 30 | * @returns true if you can summon the target monster, false otherwise 31 | */ 32 | export function canRainMan(target: Monster): boolean { 33 | if ( 34 | !have($skill`Rain Man`) || // having the skill implies you are in heavy rains path 35 | myRain() < 50 || 36 | !target.copyable || 37 | target.id < 0 38 | ) { 39 | return false; 40 | } 41 | if (monsterFactoidsAvailable(target, false) > 0) { 42 | return true; 43 | } 44 | 45 | const page = withChoice(970, 2, () => 46 | visitUrl( 47 | `runskillz.php?pwd&action=Skillz&whichskill=${ 48 | $skill`Rain Man`.id 49 | }&quantity=1`, 50 | ), 51 | ); 52 | return page.indexOf(`