├── .eslintignore ├── .prettierrc.js ├── .gitignore ├── .prettierignore ├── .vscode ├── settings.json └── tasks.json ├── babel.config.js ├── src ├── engine │ ├── task.ts │ ├── combat.ts │ ├── outfit.ts │ ├── moods.ts │ ├── engine.ts │ └── resources.ts ├── lib.ts ├── tasks │ ├── level1.ts │ ├── level2.ts │ ├── level4.ts │ ├── level5.ts │ ├── level3.ts │ ├── all.ts │ ├── level10.ts │ ├── level8.ts │ ├── level6.ts │ ├── level9.ts │ ├── level11.ts │ ├── level11_manor.ts │ ├── level11_palindome.ts │ ├── level7.ts │ ├── leveling.ts │ ├── level11_hidden.ts │ ├── level13.ts │ ├── diet.ts │ └── misc.ts ├── route.ts └── main.ts ├── tsconfig.json ├── .eslintrc.json ├── README.md ├── .github └── workflows │ └── release-gyou.yml ├── package.json ├── webpack.config.js └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | /KoLmafia/scripts/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | KoLmafia 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Generated bundles should not be formatted 2 | /KoLmafia/scripts/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.tabSize": 2, 4 | "editor.formatOnSave": true 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = function (api) { 3 | api.cache(true); 4 | return { 5 | presets: [ 6 | "@babel/preset-typescript", 7 | [ 8 | "@babel/preset-env", 9 | { 10 | targets: { rhino: "1.7.13" }, 11 | }, 12 | ], 13 | ], 14 | plugins: [ 15 | "@babel/plugin-proposal-class-properties", 16 | "@babel/plugin-proposal-object-rest-spread", 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/engine/task.ts: -------------------------------------------------------------------------------- 1 | import { CombatActions, CombatStrategy } from "./combat"; 2 | import { Quest as BaseQuest, Task as BaseTask, Limit } from "grimoire-kolmafia"; 3 | 4 | export type Quest = BaseQuest; 5 | export type Task = { 6 | priority?: () => boolean; 7 | combat?: CombatStrategy; 8 | delay?: number | (() => number); 9 | freeaction?: boolean; 10 | freecombat?: boolean; 11 | limit: Limit; 12 | noadventures?: boolean; 13 | boss?: boolean; 14 | } & BaseTask; 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: build", 13 | "detail": "yarn run build:types && yarn run build:js" 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "lint", 18 | "problemMatcher": [], 19 | "label": "npm: lint", 20 | "detail": "eslint src && prettier --check .", 21 | "group": { 22 | "kind": "test", 23 | "isDefault": true 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "noEmit": true, 5 | "allowUnreachableCode": false, 6 | "allowUnusedLabels": false, 7 | "declaration": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["es2018"], 10 | "module": "commonjs", 11 | "noEmitOnError": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "pretty": true, 15 | "strict": true, 16 | "target": "es2018" 17 | }, 18 | "include": ["src/**/*.ts", "test/**/*.ts"], 19 | "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | } 21 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import { print } from "kolmafia"; 2 | 3 | export function debug(message: string, color?: string): void { 4 | if (color) { 5 | print(message, color); 6 | } else { 7 | print(message); 8 | } 9 | } 10 | 11 | // From phccs 12 | export function convertMilliseconds(milliseconds: number): string { 13 | const seconds = milliseconds / 1000; 14 | const minutes = Math.floor(seconds / 60); 15 | const secondsLeft = Math.round((seconds - minutes * 60) * 1000) / 1000; 16 | const hours = Math.floor(minutes / 60); 17 | const minutesLeft = Math.round(minutes - hours * 60); 18 | return ( 19 | (hours !== 0 ? `${hours} hours, ` : "") + 20 | (minutesLeft !== 0 ? `${minutesLeft} minutes, ` : "") + 21 | (secondsLeft !== 0 ? `${secondsLeft} seconds` : "") 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint", "libram"], 9 | "rules": { 10 | "block-scoped-var": "error", 11 | "eol-last": "error", 12 | "eqeqeq": "error", 13 | "no-trailing-spaces": "error", 14 | "no-var": "error", 15 | "prefer-arrow-callback": "error", 16 | "prefer-const": "error", 17 | "prefer-template": "error", 18 | "sort-imports": [ 19 | "error", 20 | { 21 | "ignoreCase": true, 22 | "ignoreDeclarationSort": true 23 | } 24 | ], 25 | "no-unused-vars": "off", 26 | "@typescript-eslint/no-unused-vars": "error", 27 | "libram/verify-constants": "error" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a casual script designed to work if you are me, [Kasekopf (#1210810)](https://cheesellc.com/kol/profile.php?u=Kasekopf). I mostly run as a Seal Clubber. 4 | 5 | ### Strategy 6 | 7 | The script is designed to be run as part of a loop. In particular, it expects that something like [garbo](https://github.com/Loathing-Associates-Scripting-Society/garbage-collector) will use the rest of the turns. This means that profitable daily resources (e.g. copiers) are avoided, but other resources (free runaways, kills, some wanderers) are used to save turns where possible. 8 | 9 | ### Installation 10 | 11 | This script is not currently in a state where it will work for most users out of the box. You may need to make a few custom typescript modifications. Thus it cannot yet be checked out through the mafia GUI. 12 | 13 | 1. Compile the script, following instructions in the [kol-ts-starter](https://github.com/docrostov/kol-ts-starter). 14 | 2. Copy loopcasual.js from KoLmafia/scripts/loop-casual to your Mafia scripts directory. 15 | -------------------------------------------------------------------------------- /src/tasks/level1.ts: -------------------------------------------------------------------------------- 1 | import { use, visitUrl } from "kolmafia"; 2 | import { $item, have } from "libram"; 3 | import { Quest } from "../engine/task"; 4 | import { step } from "grimoire-kolmafia"; 5 | 6 | export const TootQuest: Quest = { 7 | name: "Toot", 8 | tasks: [ 9 | { 10 | name: "Start", 11 | after: [], 12 | completed: () => step("questM05Toot") !== -1, 13 | do: () => visitUrl("council.php"), 14 | limit: { tries: 1 }, 15 | freeaction: true, 16 | }, 17 | { 18 | name: "Toot", 19 | after: ["Start"], 20 | completed: () => step("questM05Toot") > 0, 21 | do: () => visitUrl("tutorial.php?action=toot"), 22 | limit: { tries: 1 }, 23 | freeaction: true, 24 | }, 25 | { 26 | name: "Finish", 27 | after: ["Toot"], 28 | completed: () => step("questM05Toot") > 0 && !have($item`letter from King Ralph XI`), 29 | do: () => use($item`letter from King Ralph XI`), 30 | limit: { tries: 1 }, 31 | freeaction: true, 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /.github/workflows/release-gyou.yml: -------------------------------------------------------------------------------- 1 | name: Release loopgyou 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | ref: "gyou" 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Build 25 | run: | 26 | yarn install 27 | echo "export const lastCommitHash: string | undefined = \"$(git rev-parse --short HEAD)\";" > src/_git_commit.ts 28 | yarn run build 29 | git checkout src/_git_commit.ts 30 | git checkout yarn.lock 31 | - run: | 32 | git config user.name "Build Script" 33 | git config user.email "<>" 34 | git fetch --all 35 | SHA=$(git rev-parse --short HEAD) 36 | git switch release 37 | rm -rf scripts/ 38 | rm -rf ccs/ 39 | rm -rf relay/ 40 | mv KoLmafia/* ./ 41 | rm -rf KoLmafia 42 | git add scripts/ 43 | git add relay/ 44 | git commit -m "Build Mafia files for commit $SHA" 45 | git push origin release 46 | -------------------------------------------------------------------------------- /src/tasks/level2.ts: -------------------------------------------------------------------------------- 1 | import { myLevel, visitUrl } from "kolmafia"; 2 | import { $location } from "libram"; 3 | import { Quest } from "../engine/task"; 4 | import { step } from "grimoire-kolmafia"; 5 | 6 | export const MosquitoQuest: Quest = { 7 | name: "Mosquito", 8 | tasks: [ 9 | { 10 | name: "Start", 11 | after: ["Toot/Finish"], 12 | ready: () => myLevel() >= 2, 13 | completed: () => step("questL02Larva") !== -1, 14 | do: () => visitUrl("council.php"), 15 | limit: { tries: 1 }, 16 | freeaction: true, 17 | }, 18 | { 19 | name: "Burn Delay", 20 | after: ["Start"], 21 | completed: () => $location`The Spooky Forest`.turnsSpent >= 5 || step("questL02Larva") >= 1, 22 | do: $location`The Spooky Forest`, 23 | choices: { 502: 2, 505: 1, 334: 1 }, 24 | limit: { tries: 5 }, 25 | delay: 5, 26 | }, 27 | { 28 | name: "Mosquito", 29 | after: ["Burn Delay"], 30 | completed: () => step("questL02Larva") >= 1, 31 | do: $location`The Spooky Forest`, 32 | choices: { 502: 2, 505: 1, 334: 1 }, 33 | outfit: { modifier: "-combat" }, 34 | limit: { soft: 20 }, 35 | }, 36 | { 37 | name: "Finish", 38 | after: ["Mosquito"], 39 | completed: () => step("questL02Larva") === 999, 40 | do: () => visitUrl("council.php"), 41 | limit: { tries: 1 }, 42 | freeaction: true, 43 | }, 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /src/tasks/level4.ts: -------------------------------------------------------------------------------- 1 | import { myLevel, use, visitUrl } from "kolmafia"; 2 | import { $item, $location, $monster } from "libram"; 3 | import { Quest } from "../engine/task"; 4 | import { CombatStrategy } from "../engine/combat"; 5 | import { step } from "grimoire-kolmafia"; 6 | 7 | export const BatQuest: Quest = { 8 | name: "Bat", 9 | tasks: [ 10 | { 11 | name: "Start", 12 | after: ["Toot/Finish"], 13 | ready: () => myLevel() >= 4, 14 | completed: () => step("questL04Bat") !== -1, 15 | do: () => visitUrl("council.php"), 16 | limit: { tries: 1 }, 17 | freeaction: true, 18 | }, 19 | { 20 | name: "Use Sonar", 21 | after: ["Start"], 22 | acquire: [{ item: $item`sonar-in-a-biscuit` }], 23 | completed: () => step("questL04Bat") >= 3, 24 | do: () => use($item`sonar-in-a-biscuit`), 25 | limit: { tries: 3 }, 26 | freeaction: true, 27 | }, 28 | { 29 | name: "Boss Bat", 30 | after: ["Use Sonar"], 31 | completed: () => step("questL04Bat") >= 4, 32 | do: $location`The Boss Bat's Lair`, 33 | combat: new CombatStrategy().kill($monster`Boss Bat`).ignoreNoBanish(), 34 | limit: { soft: 10 }, 35 | delay: 6, 36 | }, 37 | { 38 | name: "Finish", 39 | after: ["Boss Bat"], 40 | completed: () => step("questL04Bat") === 999, 41 | do: () => visitUrl("council.php"), 42 | limit: { tries: 1 }, 43 | freeaction: true, 44 | }, 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /src/tasks/level5.ts: -------------------------------------------------------------------------------- 1 | import { myLevel, use, visitUrl } from "kolmafia"; 2 | import { $effects, $item, $location, $monster, have } from "libram"; 3 | import { Quest } from "../engine/task"; 4 | import { CombatStrategy } from "../engine/combat"; 5 | import { step } from "grimoire-kolmafia"; 6 | 7 | export const KnobQuest: Quest = { 8 | name: "Knob", 9 | tasks: [ 10 | { 11 | name: "Start", 12 | after: ["Toot/Finish"], 13 | ready: () => myLevel() >= 5, 14 | completed: () => step("questL05Goblin") >= 0, 15 | do: () => visitUrl("council.php"), 16 | limit: { tries: 1 }, 17 | freeaction: true, 18 | }, 19 | { 20 | name: "Outskirts", 21 | after: [], 22 | completed: () => have($item`Knob Goblin encryption key`) || step("questL05Goblin") > 0, 23 | do: $location`The Outskirts of Cobb's Knob`, 24 | choices: { 111: 3, 113: 2, 118: 1 }, 25 | limit: { tries: 13 }, 26 | delay: 10, 27 | }, 28 | { 29 | name: "Open Knob", 30 | after: ["Start", "Outskirts"], 31 | completed: () => step("questL05Goblin") >= 1, 32 | do: () => use($item`Cobb's Knob map`), 33 | limit: { tries: 1 }, 34 | freeaction: true, 35 | }, 36 | { 37 | name: "King", 38 | after: ["Open Knob"], 39 | acquire: [ 40 | { item: $item`Knob Goblin harem veil` }, 41 | { item: $item`Knob Goblin harem pants` }, 42 | { item: $item`Knob Goblin perfume` }, 43 | ], 44 | completed: () => step("questL05Goblin") === 999, 45 | do: $location`Throne Room`, 46 | boss: true, 47 | combat: new CombatStrategy().kill($monster`Knob Goblin King`), 48 | effects: $effects`Knob Goblin Perfume`, 49 | limit: { tries: 1 }, 50 | }, 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loop-casual", 3 | "version": "0.1.0", 4 | "description": "Casual script", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "yarn run build:types && yarn run build:js", 9 | "build:types": "tsc", 10 | "build:js": "webpack", 11 | "lint": "eslint src && prettier --check .", 12 | "lint:fix": "eslint src --fix && prettier --check --write .", 13 | "watch": "webpack --watch --progress" 14 | }, 15 | "devDependencies": { 16 | "@babel/cli": "^7.14.8", 17 | "@babel/core": "^7.15.0", 18 | "@babel/plugin-proposal-class-properties": "^7.14.5", 19 | "@babel/plugin-proposal-object-rest-spread": "^7.14.7", 20 | "@babel/preset-env": "^7.15.0", 21 | "@babel/preset-typescript": "^7.15.0", 22 | "@typescript-eslint/eslint-plugin": "^5.9.1", 23 | "@typescript-eslint/parser": "^5.9.1", 24 | "babel-loader": "^8.2.2", 25 | "eslint": "^8.7.0", 26 | "eslint-config-prettier": "^8.3.0", 27 | "eslint-plugin-libram": "^0.4.9", 28 | "prettier": "^2.3.2", 29 | "typescript": "^4.4.2", 30 | "webpack": "^5.51.1", 31 | "webpack-cli": "^4.8.0" 32 | }, 33 | "dependencies": { 34 | "core-js": "^3.16.4", 35 | "grimoire-kolmafia": "0.3.17", 36 | "kolmafia": "^5.27321.0", 37 | "libram": "^0.7.17" 38 | }, 39 | "author": "Kasekopf", 40 | "license": "ISC", 41 | "repository": "https://github.com/Kasekopf/loop-casual", 42 | "keywords": [ 43 | "KoLMafia", 44 | "JS", 45 | "TS" 46 | ], 47 | "bugs": { 48 | "url": "https://github.com/Kasekopf/loop-casual/issues" 49 | }, 50 | "homepage": "https://github.com/Kasekopf/loop-casual", 51 | "private": true, 52 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 53 | } 54 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | const path = require("path"); 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | const webpack = require("webpack"); // does this have a purpose? or can it just get deleted? 7 | const packageData = require("./package.json"); 8 | /* eslint-enable @typescript-eslint/no-var-requires */ 9 | 10 | module.exports = { 11 | entry: { 12 | // Define files webpack will emit, does not need to correspond 1:1 with every typescript file 13 | // You need an emitted file for each entrypoint into your code, e.g. the main script and the ccs or ccs consult script it calls 14 | loopcasual: "./src/main.ts", 15 | }, 16 | // Turns on tree-shaking and minification in the default Terser minifier 17 | // https://webpack.js.org/plugins/terser-webpack-plugin/ 18 | mode: "production", 19 | devtool: false, 20 | output: { 21 | path: path.resolve(__dirname, "KoLmafia", "scripts", packageData.name), 22 | filename: "[name].js", 23 | libraryTarget: "commonjs", 24 | }, 25 | resolve: { 26 | extensions: [".ts", ".tsx", ".js", ".json"], 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | // Include ts, tsx, js, and jsx files. 32 | test: /\.(ts|js)x?$/, 33 | // exclude: /node_modules/, 34 | loader: "babel-loader", 35 | }, 36 | ], 37 | }, 38 | optimization: { 39 | // Disable compression because it makes debugging more difficult for KolMafia 40 | minimize: false, 41 | }, 42 | performance: { 43 | // Disable the warning about assets exceeding the recommended size because this isn't a website script 44 | hints: false, 45 | }, 46 | plugins: [], 47 | externals: { 48 | // Necessary to allow kolmafia imports. 49 | kolmafia: "commonjs kolmafia", 50 | // Add any ASH scripts you would like to use here to allow importing. E.g.: 51 | // "canadv.ash": "commonjs canadv.ash", 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/tasks/level3.ts: -------------------------------------------------------------------------------- 1 | import { getProperty, myLevel, runChoice, runCombat, visitUrl } from "kolmafia"; 2 | import { $effects } from "libram"; 3 | import { CombatStrategy } from "../engine/combat"; 4 | import { Quest } from "../engine/task"; 5 | import { step } from "grimoire-kolmafia"; 6 | 7 | export const TavernQuest: Quest = { 8 | name: "Tavern", 9 | tasks: [ 10 | { 11 | name: "Start", 12 | after: ["Mosquito/Finish"], 13 | ready: () => myLevel() >= 3, 14 | completed: () => step("questL03Rat") >= 0, 15 | do: () => visitUrl("council.php"), 16 | limit: { tries: 1 }, 17 | freeaction: true, 18 | }, 19 | { 20 | name: "Tavernkeep", 21 | after: ["Start"], 22 | completed: () => step("questL03Rat") >= 1, 23 | do: () => visitUrl("tavern.php?place=barkeep"), 24 | limit: { tries: 1 }, 25 | freeaction: true, 26 | }, 27 | { 28 | name: "Basement", 29 | after: ["Tavernkeep"], 30 | completed: () => step("questL03Rat") >= 2, 31 | do: (): void => { 32 | visitUrl("cellar.php"); 33 | const layout = getProperty("tavernLayout"); 34 | const path = [3, 2, 1, 0, 5, 10, 15, 20, 16, 21]; 35 | for (let i = 0; i < path.length; i++) { 36 | if (layout.charAt(path[i]) === "0") { 37 | visitUrl(`cellar.php?action=explore&whichspot=${path[i] + 1}`); 38 | runCombat(); 39 | runChoice(-1); 40 | break; 41 | } 42 | } 43 | }, 44 | effects: $effects`Belch the Rainbow™, Benetton's Medley of Diversity`, 45 | outfit: { modifier: "-combat" }, 46 | combat: new CombatStrategy().ignoreNoBanish(), 47 | choices: { 509: 1, 510: 1, 511: 2, 514: 2, 515: 2, 496: 2, 513: 2 }, 48 | limit: { tries: 10 }, 49 | }, 50 | { 51 | name: "Finish", 52 | after: ["Basement"], 53 | completed: () => step("questL03Rat") === 999, 54 | do: () => visitUrl("tavern.php?place=barkeep"), 55 | limit: { tries: 1 }, 56 | freeaction: true, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /src/tasks/all.ts: -------------------------------------------------------------------------------- 1 | import { DietQuest } from "./diet"; 2 | import { TootQuest } from "./level1"; 3 | import { MosquitoQuest } from "./level2"; 4 | import { TavernQuest } from "./level3"; 5 | import { BatQuest } from "./level4"; 6 | import { KnobQuest } from "./level5"; 7 | import { FriarQuest, OrganQuest } from "./level6"; 8 | import { CryptQuest } from "./level7"; 9 | import { McLargeHugeQuest } from "./level8"; 10 | import { ChasmQuest } from "./level9"; 11 | import { GiantQuest } from "./level10"; 12 | import { HiddenQuest } from "./level11_hidden"; 13 | import { ManorQuest } from "./level11_manor"; 14 | import { PalindomeQuest } from "./level11_palindome"; 15 | import { MacguffinQuest } from "./level11"; 16 | import { WarQuest } from "./level12"; 17 | import { TowerQuest } from "./level13"; 18 | import { DigitalQuest, KeysQuest, MiscQuest } from "./misc"; 19 | import { Task } from "../engine/task"; 20 | import { LevelingQuest } from "./leveling"; 21 | import { getTasks } from "grimoire-kolmafia"; 22 | 23 | export function all_tasks(): Task[] { 24 | const quests = [ 25 | TootQuest, 26 | LevelingQuest, 27 | MiscQuest, 28 | KeysQuest, 29 | DietQuest, 30 | MosquitoQuest, 31 | TavernQuest, 32 | BatQuest, 33 | KnobQuest, 34 | FriarQuest, 35 | OrganQuest, 36 | CryptQuest, 37 | McLargeHugeQuest, 38 | ChasmQuest, 39 | GiantQuest, 40 | HiddenQuest, 41 | ManorQuest, 42 | PalindomeQuest, 43 | MacguffinQuest, 44 | WarQuest, 45 | DigitalQuest, 46 | TowerQuest, 47 | ]; 48 | return getTasks(quests); 49 | } 50 | 51 | export function quest_tasks(): Task[] { 52 | const quests = [ 53 | TootQuest, 54 | MiscQuest, 55 | KeysQuest, 56 | DietQuest, 57 | MosquitoQuest, 58 | TavernQuest, 59 | BatQuest, 60 | KnobQuest, 61 | FriarQuest, 62 | CryptQuest, 63 | McLargeHugeQuest, 64 | ChasmQuest, 65 | GiantQuest, 66 | HiddenQuest, 67 | ManorQuest, 68 | PalindomeQuest, 69 | MacguffinQuest, 70 | WarQuest, 71 | DigitalQuest, 72 | TowerQuest, 73 | ]; 74 | return getTasks(quests); 75 | } 76 | 77 | export function level_tasks(): Task[] { 78 | return getTasks([LevelingQuest]); 79 | } 80 | 81 | export function organ_tasks(): Task[] { 82 | return getTasks([TootQuest, FriarQuest, OrganQuest]); 83 | } 84 | -------------------------------------------------------------------------------- /src/engine/combat.ts: -------------------------------------------------------------------------------- 1 | import { 2 | equippedItem, 3 | Location, 4 | Monster, 5 | monsterDefense, 6 | monsterLevelAdjustment, 7 | myBuffedstat, 8 | weaponType, 9 | } from "kolmafia"; 10 | import { $item, $skill, $slot, $stat, Macro } from "libram"; 11 | import { ActionDefaults, CombatStrategy as BaseCombatStrategy } from "grimoire-kolmafia"; 12 | 13 | const myActions = [ 14 | "ignore", // Task doesn't care what happens 15 | "ignoreNoBanish", // Task doesn't care what happens, as long as it is not banished 16 | "kill", // Task needs to kill it, with or without a free kill 17 | "killFree", // Task needs to kill it with a free kill 18 | "killHard", // Task needs to kill it without using a free kill (i.e., boss, or already free) 19 | "banish", // Task doesn't care what happens, but banishing is useful 20 | "abort", // Abort the macro and the script; an error has occured 21 | ] as const; 22 | export type CombatActions = typeof myActions[number]; 23 | export class CombatStrategy extends BaseCombatStrategy.withActions(myActions) {} 24 | export class MyActionDefaults implements ActionDefaults { 25 | ignore() { 26 | return new Macro() 27 | .runaway() 28 | .skill($skill`Saucestorm`) 29 | .attack() 30 | .repeat(); 31 | } 32 | 33 | kill(target?: Monster | Location) { 34 | // Upgrade normal kills to hard kills if we are underleveled 35 | if ( 36 | target && 37 | target instanceof Monster && 38 | monsterDefense(target) * 1.25 > myBuffedstat(weaponType(equippedItem($slot`Weapon`))) 39 | ) 40 | return this.killHard(); 41 | 42 | if (monsterLevelAdjustment() > 150) return new Macro().skill($skill`Saucegeyser`).repeat(); 43 | if (target && target instanceof Monster && target.physicalResistance >= 70) 44 | return this.delevel() 45 | .skill($skill`Saucegeyser`) 46 | .repeat(); 47 | else return this.delevel().attack().repeat(); 48 | } 49 | 50 | killHard(target?: Monster | Location) { 51 | if ( 52 | (target && target instanceof Monster && target.physicalResistance >= 70) || 53 | weaponType(equippedItem($slot`Weapon`)) !== $stat`muscle` 54 | ) { 55 | return this.delevel() 56 | .skill($skill`Saucegeyser`) 57 | .repeat(); 58 | } else { 59 | return this.delevel() 60 | .skill($skill`Lunging Thrust-Smack`) 61 | .repeat(); 62 | } 63 | } 64 | 65 | ignoreNoBanish() { 66 | return this.ignore(); 67 | } 68 | killFree() { 69 | return this.abort(); 70 | } // Abort if no resource provided 71 | banish() { 72 | return this.abort(); 73 | } // Abort if no resource provided 74 | abort() { 75 | return new Macro().abort(); 76 | } 77 | 78 | private delevel() { 79 | return new Macro() 80 | .skill($skill`Curse of Weaksauce`) 81 | .trySkill($skill`Pocket Crumbs`) 82 | .trySkill($skill`Micrometeorite`) 83 | .tryItem($item`Rain-Doh indigo cup`) 84 | .trySkill($skill`Summon Love Mosquito`) 85 | .tryItem($item`Time-Spinner`); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/route.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "./engine/task"; 2 | import { orderByRoute } from "grimoire-kolmafia"; 3 | 4 | export const routing: string[] = [ 5 | "Diet/Numberology", // Numberology is always ready at the start of the day 6 | "Diet/Sausage", // Eat magical sausages as soon as they are obtained 7 | "Diet/Hourglass", 8 | 9 | // Pickup items 10 | "Misc/Short Cook", 11 | "Misc/Floundry", 12 | "Misc/Voting", 13 | "Misc/Acquire Kgnee", 14 | "Misc/Acquire FamEquip", 15 | 16 | // Start with the basic leveling tasks 17 | "Toot/Finish", 18 | "Leveling/Cloud Talk", 19 | "Leveling/Daycare", 20 | "Leveling/Bastille", 21 | "Leveling/Leaflet", 22 | "Leveling/Snojo", 23 | "Leveling/Chateau", 24 | 25 | // Then do the scaling leveling 26 | "Leveling/LOV Tunnel", 27 | "Leveling/Witchess", 28 | "Leveling/God Lobster", 29 | "Leveling/Machine Elf", 30 | "Leveling/Neverending Party", 31 | "Leveling/Sausage Fights", 32 | "Diet/Consume", 33 | "Misc/Protonic Ghost", // whenever ghosts are ready 34 | 35 | // Open up MacGuffin zones 36 | "Macguffin/Diary", 37 | "Macguffin/Desert", // charge camel, use voters 38 | 39 | // Line up noncombats 40 | "Manor/Billiards", 41 | "War/Enrage", 42 | "War/Flyers End", // Turn in flyers ASAP in-case of tracking issues 43 | "Giant/Airship", 44 | "Friar/Finish", 45 | "Crypt/Cranny", 46 | "Mosquito/Mosquito", 47 | "Hidden City/Open Temple", 48 | "Tavern/Finish", 49 | "Giant/Basement Finish", 50 | 51 | // Burn delay to unlock remaining noncombats 52 | "Palindome/Copperhead", 53 | "Palindome/Bat Snake", 54 | "Palindome/Cold Snake", 55 | "Giant/Ground", 56 | "Palindome/Zepplin", 57 | "Manor/Bedroom", 58 | "Manor/Bathroom Delay", 59 | "Manor/Gallery Delay", 60 | 61 | // Line up more noncombats 62 | "Manor/Gallery", // Gallery first in-case we banished Out in the Garden 63 | "Giant/Top Floor", 64 | "Manor/Bathroom", 65 | "Manor/Ballroom", 66 | 67 | // Detour to route Steely-Eyed Squint 68 | "Manor/Wine Cellar", 69 | "Manor/Laundry Room", 70 | 71 | // Finish noncombats, now with freekills available 72 | "Palindome/Alarm Gem", 73 | 74 | // Use Hidden City to charge camel 75 | "Hidden City/Open Bowling", 76 | "Hidden City/Open Office", 77 | "Hidden City/Open Hospital", 78 | "Hidden City/Open Apartment", 79 | 80 | // Nostalgia chaining 81 | "Orc Chasm/ABoo Start", 82 | "Crypt/Nook", 83 | "Orc Chasm/ABoo Peak", 84 | 85 | "Hidden City/Apartment", // Get this out of the way 86 | "Macguffin/Open Pyramid", // Open more delay for lategame 87 | 88 | // Non-delay quests 89 | "Mosquito/Finish", 90 | "Tavern/Finish", 91 | "Bat/Use Sonar", 92 | "Crypt/Finish", 93 | "McLargeHuge/Finish", 94 | "Orc Chasm/Finish", 95 | "Giant/Finish", 96 | "War/Boss Hippie", 97 | "War/Boss Frat", 98 | 99 | // Finish up with last delay 100 | "Macguffin/Finish", 101 | "Knob/King", 102 | "Bat/Finish", 103 | 104 | // Obtain available keys before attempting the daily dungeon 105 | "Keys/Deck", 106 | "Keys/Lockpicking", 107 | 108 | "Tower/Finish", 109 | "Organ/Finish", // Organ last, just so it doesn't appear in turncount 110 | ]; 111 | 112 | export function prioritize(tasks: Task[], ignore_missing_tasks?: boolean): Task[] { 113 | return orderByRoute(tasks, routing, ignore_missing_tasks); 114 | } 115 | -------------------------------------------------------------------------------- /src/tasks/level10.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, containsText, myLevel, use, visitUrl } from "kolmafia"; 2 | import { $effect, $item, $items, $location, $monster, have } from "libram"; 3 | import { CombatStrategy } from "../engine/combat"; 4 | import { Quest } from "../engine/task"; 5 | import { step } from "grimoire-kolmafia"; 6 | import { shenItem } from "./level11_palindome"; 7 | 8 | export const GiantQuest: Quest = { 9 | name: "Giant", 10 | tasks: [ 11 | { 12 | name: "Start", 13 | after: ["Toot/Finish"], 14 | ready: () => myLevel() >= 10, 15 | completed: () => step("questL10Garbage") !== -1, 16 | do: () => visitUrl("council.php"), 17 | limit: { tries: 1 }, 18 | freeaction: true, 19 | }, 20 | { 21 | name: "Grow Beanstalk", 22 | after: ["Start"], 23 | acquire: [{ item: $item`enchanted bean` }], 24 | completed: () => step("questL10Garbage") >= 1, 25 | do: () => use($item`enchanted bean`), 26 | limit: { tries: 1 }, 27 | freeaction: true, 28 | }, 29 | { 30 | name: "Airship", 31 | after: ["Grow Beanstalk"], 32 | completed: () => have($item`S.O.C.K.`), 33 | do: $location`The Penultimate Fantasy Airship`, 34 | choices: { 178: 2, 182: 1 }, 35 | post: () => { 36 | if (have($effect`Temporary Amnesia`)) cliExecute("uneffect Temporary Amnesia"); 37 | }, 38 | outfit: { modifier: "-combat" }, 39 | limit: { soft: 50 }, 40 | delay: () => 41 | have($item`Plastic Wrap Immateria`) ? 25 : have($item`Gauze Immateria`) ? 20 : 15, // After that, just look for noncombats 42 | }, 43 | { 44 | name: "Basement Search", 45 | after: ["Airship"], 46 | completed: () => 47 | containsText( 48 | $location`The Castle in the Clouds in the Sky (Basement)`.noncombatQueue, 49 | "Mess Around with Gym" 50 | ) || step("questL10Garbage") >= 8, 51 | do: $location`The Castle in the Clouds in the Sky (Basement)`, 52 | outfit: { modifier: "-combat" }, 53 | limit: { soft: 20 }, 54 | choices: { 670: 5, 669: 1, 671: 4 }, 55 | }, 56 | { 57 | name: "Basement Finish", 58 | after: ["Basement Search"], 59 | acquire: [{ item: $item`amulet of extreme plot significance` }], 60 | completed: () => step("questL10Garbage") >= 8, 61 | do: $location`The Castle in the Clouds in the Sky (Basement)`, 62 | outfit: { equip: $items`amulet of extreme plot significance` }, 63 | choices: { 670: 4 }, 64 | limit: { tries: 1 }, 65 | }, 66 | { 67 | name: "Ground", 68 | after: ["Basement Finish"], 69 | completed: () => step("questL10Garbage") >= 9, 70 | do: $location`The Castle in the Clouds in the Sky (Ground Floor)`, 71 | choices: { 672: 3, 673: 3, 674: 3, 1026: 3 }, 72 | limit: { turns: 11 }, 73 | delay: 10, 74 | }, 75 | { 76 | name: "Top Floor", 77 | after: ["Ground"], 78 | acquire: [{ item: $item`Mohawk wig` }], 79 | completed: () => step("questL10Garbage") >= 10, 80 | do: $location`The Castle in the Clouds in the Sky (Top Floor)`, 81 | outfit: { equip: $items`Mohawk wig`, modifier: "-combat" }, 82 | combat: new CombatStrategy().killHard($monster`Burning Snake of Fire`), 83 | choices: { 675: 4, 676: 4, 677: 4, 678: 1, 679: 1, 1431: 4 }, 84 | limit: { soft: 20 }, 85 | }, 86 | { 87 | name: "Unlock HITS", 88 | after: ["Top Floor"], 89 | ready: () => shenItem($item`The Eye of the Stars`), 90 | completed: () => have($item`steam-powered model rocketship`), 91 | do: $location`The Castle in the Clouds in the Sky (Top Floor)`, 92 | outfit: { modifier: "-combat" }, 93 | combat: new CombatStrategy().killHard($monster`Burning Snake of Fire`), 94 | choices: { 675: 4, 676: 4, 677: 2, 678: 3, 679: 1, 1431: 4 }, 95 | limit: { soft: 20 }, 96 | }, 97 | { 98 | name: "Finish", 99 | after: ["Top Floor"], 100 | completed: () => step("questL10Garbage") === 999, 101 | do: () => visitUrl("council.php"), 102 | limit: { soft: 10 }, 103 | freeaction: true, 104 | }, 105 | ], 106 | }; 107 | -------------------------------------------------------------------------------- /src/tasks/level8.ts: -------------------------------------------------------------------------------- 1 | import { myLevel, visitUrl } from "kolmafia"; 2 | import { $item, $items, $location, get, have } from "libram"; 3 | import { Quest } from "../engine/task"; 4 | import { CombatStrategy } from "../engine/combat"; 5 | import { step } from "grimoire-kolmafia"; 6 | 7 | export const McLargeHugeQuest: Quest = { 8 | name: "McLargeHuge", 9 | tasks: [ 10 | { 11 | name: "Start", 12 | after: ["Toot/Finish"], 13 | ready: () => myLevel() >= 8, 14 | completed: () => step("questL08Trapper") !== -1, 15 | do: () => visitUrl("council.php"), 16 | limit: { tries: 1 }, 17 | freeaction: true, 18 | }, 19 | { 20 | name: "Ores", 21 | after: ["Start"], 22 | acquire: [ 23 | { item: $item`asbestos ore`, num: 3 }, 24 | { item: $item`chrome ore`, num: 3 }, 25 | { item: $item`linoleum ore`, num: 3 }, 26 | { item: $item`goat cheese`, num: 3 }, 27 | ], 28 | completed: () => step("questL08Trapper") >= 2, 29 | do: (): void => { 30 | visitUrl("place.php?whichplace=mclargehuge&action=trappercabin"); // request ore 31 | visitUrl("place.php?whichplace=mclargehuge&action=trappercabin"); // provide 32 | }, 33 | limit: { tries: 1 }, 34 | freeaction: true, 35 | }, 36 | { 37 | name: "Extreme Snowboard", 38 | after: ["Ores"], 39 | acquire: [ 40 | { item: $item`eXtreme mittens` }, 41 | { item: $item`snowboarder pants` }, 42 | { item: $item`eXtreme scarf` }, 43 | ], 44 | completed: () => get("currentExtremity") >= 3 || step("questL08Trapper") >= 3, 45 | do: $location`The eXtreme Slope`, 46 | outfit: () => { 47 | if (haveHugeLarge()) 48 | return { 49 | // eslint-disable-next-line libram/verify-constants 50 | equip: $items`McHugeLarge left pole, McHugeLarge right pole, McHugeLarge left ski, McHugeLarge right ski, McHugeLarge duffel bag`, 51 | modifier: "-combat", 52 | }; 53 | return { 54 | equip: $items`eXtreme mittens, snowboarder pants, eXtreme scarf`, 55 | modifier: "-combat", 56 | }; 57 | }, 58 | limit: { soft: 30 }, 59 | }, 60 | { 61 | name: "Climb", 62 | after: ["Ores", "Extreme Snowboard"], 63 | completed: () => step("questL08Trapper") >= 3, 64 | do: (): void => { 65 | visitUrl("place.php?whichplace=mclargehuge&action=cloudypeak"); 66 | }, 67 | outfit: () => { 68 | if (haveHugeLarge()) 69 | return { 70 | // eslint-disable-next-line libram/verify-constants 71 | equip: $items`McHugeLarge left pole, McHugeLarge right pole, McHugeLarge left ski, McHugeLarge right ski, McHugeLarge duffel bag`, 72 | modifier: "-combat", 73 | }; 74 | return { 75 | equip: $items`eXtreme mittens, snowboarder pants, eXtreme scarf`, 76 | modifier: "-combat", 77 | }; 78 | }, 79 | limit: { tries: 1 }, 80 | freeaction: true, 81 | }, 82 | { 83 | name: "Peak", 84 | after: ["Climb"], 85 | completed: () => step("questL08Trapper") >= 5, 86 | do: $location`Mist-Shrouded Peak`, 87 | outfit: { modifier: "cold res 5min" }, 88 | boss: true, 89 | combat: new CombatStrategy().kill(), 90 | limit: { tries: 4 }, 91 | }, 92 | { 93 | name: "Finish", 94 | after: ["Peak"], 95 | completed: () => step("questL08Trapper") === 999, 96 | do: () => visitUrl("place.php?whichplace=mclargehuge&action=trappercabin"), 97 | limit: { tries: 1 }, 98 | freeaction: true, 99 | }, 100 | ], 101 | }; 102 | 103 | function haveHugeLarge() { 104 | return ( 105 | // eslint-disable-next-line libram/verify-constants 106 | have($item`McHugeLarge left pole`) && 107 | // eslint-disable-next-line libram/verify-constants 108 | have($item`McHugeLarge right pole`) && 109 | // eslint-disable-next-line libram/verify-constants 110 | have($item`McHugeLarge left ski`) && 111 | // eslint-disable-next-line libram/verify-constants 112 | have($item`McHugeLarge right ski`) && 113 | // eslint-disable-next-line libram/verify-constants 114 | have($item`McHugeLarge duffel bag`) 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/tasks/level6.ts: -------------------------------------------------------------------------------- 1 | import { drink, Item, itemAmount, myLevel, toInt, visitUrl } from "kolmafia"; 2 | import { $item, $items, $location, $monsters, $skill, have } from "libram"; 3 | import { CombatStrategy } from "../engine/combat"; 4 | import { Quest } from "../engine/task"; 5 | import { step } from "grimoire-kolmafia"; 6 | 7 | export const FriarQuest: Quest = { 8 | name: "Friar", 9 | tasks: [ 10 | { 11 | name: "Start", 12 | after: ["Toot/Finish"], 13 | ready: () => myLevel() >= 6, 14 | completed: () => step("questL06Friar") !== -1, 15 | do: () => visitUrl("council.php"), 16 | limit: { tries: 1 }, 17 | freeaction: true, 18 | }, 19 | { 20 | name: "Heart", 21 | after: ["Start"], 22 | completed: () => have($item`box of birthday candles`) || step("questL06Friar") === 999, 23 | do: $location`The Dark Heart of the Woods`, 24 | outfit: { modifier: "-combat" }, 25 | limit: { soft: 20 }, 26 | }, 27 | { 28 | name: "Neck", 29 | after: ["Start"], 30 | completed: () => have($item`dodecagram`) || step("questL06Friar") === 999, 31 | do: $location`The Dark Neck of the Woods`, 32 | outfit: { modifier: "-combat" }, 33 | choices: { 1428: 2 }, 34 | limit: { soft: 20 }, 35 | }, 36 | { 37 | name: "Elbow", 38 | after: ["Start"], 39 | completed: () => have($item`eldritch butterknife`) || step("questL06Friar") === 999, 40 | do: $location`The Dark Elbow of the Woods`, 41 | outfit: { modifier: "-combat" }, 42 | limit: { soft: 20 }, 43 | }, 44 | { 45 | name: "Finish", 46 | after: ["Heart", "Neck", "Elbow"], 47 | completed: () => step("questL06Friar") === 999, 48 | do: () => visitUrl("friars.php?action=ritual&pwd"), 49 | limit: { tries: 1 }, 50 | freeaction: true, 51 | }, 52 | ], 53 | }; 54 | 55 | export const OrganQuest: Quest = { 56 | name: "Organ", 57 | tasks: [ 58 | { 59 | name: "Start", 60 | after: ["Friar/Finish"], 61 | completed: () => step("questM10Azazel") >= 0, 62 | do: (): void => { 63 | visitUrl("pandamonium.php?action=temp"); 64 | visitUrl("pandamonium.php?action=sven"); 65 | }, 66 | limit: { tries: 1 }, 67 | freeaction: true, 68 | }, 69 | { 70 | name: "Tutu", 71 | after: ["Start"], 72 | completed: () => have($item`Azazel's tutu`) || step("questM10Azazel") === 999, 73 | acquire: [ 74 | { item: $item`imp air`, num: 5 }, 75 | { item: $item`bus pass`, num: 5 }, 76 | ], 77 | do: () => visitUrl("pandamonium.php?action=moan"), 78 | limit: { tries: 2 }, 79 | freeaction: true, 80 | }, 81 | { 82 | name: "Arena", 83 | after: ["Start"], 84 | completed: (): boolean => { 85 | if (step("questM10Azazel") === 999) return true; 86 | if (have($item`Azazel's unicorn`)) return true; 87 | 88 | const count = (items: Item[]) => items.reduce((sum, item) => sum + itemAmount(item), 0); 89 | if (count($items`giant marshmallow, beer-scented teddy bear, gin-soaked blotter paper`) < 2) 90 | return false; 91 | if (count($items`booze-soaked cherry, comfy pillow, sponge cake`) < 2) return false; 92 | return true; 93 | }, 94 | do: $location`Infernal Rackets Backstage`, 95 | limit: { soft: 30 }, 96 | outfit: { modifier: "-combat" }, 97 | }, 98 | { 99 | name: "Unicorn", 100 | after: ["Arena"], 101 | completed: () => have($item`Azazel's unicorn`) || step("questM10Azazel") === 999, 102 | do: (): void => { 103 | const goals: { [name: string]: Item[] } = { 104 | Bognort: $items`giant marshmallow, gin-soaked blotter paper`, 105 | Stinkface: $items`beer-scented teddy bear, gin-soaked blotter paper`, 106 | Flargwurm: $items`booze-soaked cherry, sponge cake`, 107 | Jim: $items`comfy pillow, sponge cake`, 108 | }; 109 | visitUrl("pandamonium.php?action=sven"); 110 | for (const member of Object.keys(goals)) { 111 | if (goals[member].length === 0) throw `Unable to solve Azazel's arena quest`; 112 | const item = have(goals[member][0]) ? toInt(goals[member][0]) : toInt(goals[member][1]); 113 | visitUrl(`pandamonium.php?action=sven&bandmember=${member}&togive=${item}&preaction=try`); 114 | } 115 | }, 116 | limit: { tries: 1 }, 117 | freeaction: true, 118 | }, 119 | { 120 | name: "Comedy Club", 121 | after: ["Start"], 122 | completed: () => have($item`observational glasses`), 123 | do: $location`The Laugh Floor`, 124 | outfit: { modifier: "+combat" }, 125 | combat: new CombatStrategy().kill( 126 | $monsters`Carbuncle Top, Larry of the Field of Signs, Victor the Insult Comic Hellhound` 127 | ), 128 | limit: { soft: 30 }, 129 | }, 130 | { 131 | name: "Lollipop", 132 | after: ["Comedy Club"], 133 | completed: () => have($item`Azazel's lollipop`) || step("questM10Azazel") === 999, 134 | do: () => visitUrl("pandamonium.php?action=mourn&preaction=observe"), 135 | outfit: { equip: $items`observational glasses` }, 136 | limit: { tries: 1 }, 137 | freeaction: true, 138 | noadventures: true, 139 | }, 140 | { 141 | name: "Azazel", 142 | after: ["Tutu", "Unicorn", "Lollipop"], 143 | completed: () => step("questM10Azazel") === 999, 144 | do: () => visitUrl("pandamonium.php?action=temp"), 145 | limit: { tries: 1 }, 146 | freeaction: true, 147 | noadventures: true, 148 | }, 149 | { 150 | name: "Finish", 151 | after: ["Azazel"], 152 | completed: () => have($skill`Liver of Steel`), 153 | do: () => drink($item`steel margarita`), 154 | limit: { tries: 1 }, 155 | freeaction: true, 156 | noadventures: true, 157 | }, 158 | ], 159 | }; 160 | -------------------------------------------------------------------------------- /src/engine/outfit.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, equippedAmount, Familiar, Item, myBasestat, totalTurnsPlayed } from "kolmafia"; 2 | import { $familiar, $item, $stat, get, have } from "libram"; 3 | import { Resource } from "./resources"; 4 | import { Outfit, OutfitSpec } from "grimoire-kolmafia"; 5 | 6 | export function equipFirst(outfit: Outfit, resources: T[]): T | undefined { 7 | for (const resource of resources) { 8 | if (!resource.available()) continue; 9 | if (resource.chance && resource.chance() === 0) continue; 10 | if (!outfit.canEquip(resource.equip ?? [])) continue; 11 | if (!outfit.equip(resource.equip ?? [])) continue; 12 | return resource; 13 | } 14 | return undefined; 15 | } 16 | 17 | export function equipUntilCapped(outfit: Outfit, resources: T[]): T[] { 18 | const result: T[] = []; 19 | for (const resource of resources) { 20 | if (!resource.available()) continue; 21 | if (resource.chance && resource.chance() === 0) continue; 22 | if (!outfit.canEquip(resource.equip ?? [])) continue; 23 | if (!outfit.equip(resource.equip ?? [])) continue; 24 | result.push(resource); 25 | if (resource.chance && resource.chance() === 1) break; 26 | } 27 | return result; 28 | } 29 | 30 | export function equipInitial(outfit: Outfit) { 31 | if (outfit.modifier) { 32 | // Run maximizer 33 | if (outfit.modifier.includes("item")) { 34 | if ( 35 | outfit.canEquip($item`li'l ninja costume`) && 36 | outfit.canEquip($familiar`Trick-or-Treating Tot`) 37 | ) { 38 | outfit.equip($item`li'l ninja costume`); 39 | outfit.equip($familiar`Trick-or-Treating Tot`); 40 | } else { 41 | outfit.equip($familiar`Jumpsuited Hound Dog`); 42 | } 43 | } 44 | } 45 | } 46 | 47 | export function equipDefaults(outfit: Outfit): void { 48 | if (myBasestat($stat`muscle`) >= 40) outfit.equip($item`mafia thumb ring`); 49 | outfit.equip($item`lucky gold ring`); 50 | 51 | // low priority familiars for combat frequency 52 | if (outfit.modifier?.includes("-combat")) outfit.equip($familiar`Disgeist`); 53 | if (outfit.modifier?.includes("+combat")) outfit.equip($familiar`Jumpsuited Hound Dog`); 54 | 55 | if (!outfit.modifier) { 56 | // Default outfit 57 | outfit.equip($item`Fourth of May Cosplay Saber`); 58 | if (totalTurnsPlayed() >= get("nextParanormalActivity") && get("questPAGhost") === "unstarted") 59 | outfit.equip($item`protonic accelerator pack`); 60 | outfit.equip($item`vampyric cloake`); 61 | if (myBasestat($stat`mysticality`) >= 25) outfit.equip($item`Mr. Cheeng's spectacles`); 62 | } 63 | 64 | if (get("camelSpit") < 100 && get("cyrptNookEvilness") > 25) { 65 | outfit.equip($familiar`Melodramedary`); 66 | } else if (have($familiar`Temporal Riftlet`)) { 67 | outfit.equip($familiar`Temporal Riftlet`); 68 | } else if (have($item`gnomish housemaid's kgnee`)) { 69 | outfit.equip($familiar`Reagnimated Gnome`); 70 | } else outfit.equip($familiar`Galloping Grill`); 71 | 72 | const commonFamiliarEquips = new Map([ 73 | [$familiar`Melodramedary`, $item`dromedary drinking helmet`], 74 | [$familiar`Reagnimated Gnome`, $item`gnomish housemaid's kgnee`], 75 | ]); 76 | const familiarEquip = commonFamiliarEquips.get(outfit.familiar ?? $familiar`none`); 77 | if (familiarEquip && outfit.canEquip(familiarEquip)) outfit.equip(familiarEquip); 78 | } 79 | 80 | export function fixFoldables(outfit: Outfit) { 81 | const modifiers = getModifiersFrom(outfit); 82 | 83 | // Libram outfit cache may not autofold umbrella, so we need to 84 | if (equippedAmount($item`unbreakable umbrella`) > 0) { 85 | if (modifiers.includes("-combat")) { 86 | if (get("umbrellaState") !== "cocoon") cliExecute("umbrella cocoon"); 87 | } else if ( 88 | (modifiers.includes("ML") || modifiers.toLowerCase().includes("monster level percent")) && 89 | !modifiers.match("-[\\d .]*ML") 90 | ) { 91 | if (get("umbrellaState") !== "broken") cliExecute("umbrella broken"); 92 | } else if (modifiers.includes("item")) { 93 | if (get("umbrellaState") !== "bucket style") cliExecute("umbrella bucket"); 94 | } else { 95 | if (get("umbrellaState") !== "forward-facing") cliExecute("umbrella forward"); 96 | } 97 | } 98 | 99 | // Libram outfit cache may not autofold camera, so we need to 100 | if (equippedAmount($item`backup camera`) > 0) { 101 | if ( 102 | (modifiers.includes("ML") && !modifiers.match("-[\\d .]*ML")) || 103 | modifiers.includes("exp") 104 | ) { 105 | if (get("backupCameraMode").toLowerCase() !== "ml") cliExecute("backupcamera ml"); 106 | } else if (outfit.modifier?.includes("init")) { 107 | if (get("backupCameraMode").toLowerCase() !== "init") cliExecute("backupcamera init"); 108 | } else { 109 | if (get("backupCameraMode").toLowerCase() !== "meat") cliExecute("backupcamera meat"); 110 | } 111 | if (!get("backupCameraReverserEnabled")) { 112 | cliExecute("backupcamera reverser on"); 113 | } 114 | } 115 | 116 | // Libram outfit cache may not autofold parka, so we need to 117 | // eslint-disable-next-line libram/verify-constants 118 | if (equippedAmount($item`Jurassic Parka`) > 0) { 119 | if (outfit.modifier?.includes("cold res")) { 120 | if (get("parkaMode").toLowerCase() !== "kachungasaur") cliExecute("parka kachungasaur"); 121 | } else if (outfit.modifier?.includes("stench res")) { 122 | if (get("parkaMode").toLowerCase() !== "dilophosaur") cliExecute("parka dilophosaur"); 123 | } else if (outfit.modifier?.includes("ML") && !modifiers.match("-[\\d .]*ML")) { 124 | if (get("parkaMode").toLowerCase() !== "spikolodon") cliExecute("parka spikolodon"); 125 | } else if ( 126 | (outfit.modifier?.includes("init") && !modifiers.match("-[\\d .]*init")) || 127 | outfit.modifier?.includes("-combat") 128 | ) { 129 | if (get("parkaMode").toLowerCase() !== "pterodactyl") cliExecute("parka pterodactyl"); 130 | } else { 131 | // +meat 132 | if (get("parkaMode").toLowerCase() !== "kachungasaur") cliExecute("parka kachungasaur"); 133 | } 134 | } 135 | } 136 | 137 | export function getModifiersFrom(outfit: OutfitSpec | Outfit | undefined): string { 138 | if (!outfit?.modifier) return ""; 139 | if (Array.isArray(outfit.modifier)) return outfit.modifier.join(","); 140 | return outfit.modifier; 141 | } 142 | -------------------------------------------------------------------------------- /src/engine/moods.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cliExecute, 3 | Effect, 4 | getWorkshed, 5 | haveSkill, 6 | myClass, 7 | myEffects, 8 | myMaxmp, 9 | myPrimestat, 10 | toSkill, 11 | } from "kolmafia"; 12 | import { 13 | $class, 14 | $effect, 15 | $effects, 16 | $item, 17 | $skill, 18 | $stat, 19 | AsdonMartin, 20 | ensureEffect, 21 | get, 22 | have, 23 | uneffect, 24 | } from "libram"; 25 | 26 | function getRelevantEffects(): { [modifier: string]: Effect[] } { 27 | const result = { 28 | "-combat": $effects`Smooth Movements, The Sonata of Sneakiness`, 29 | "+combat": $effects`Carlweather's Cantata of Confrontation, Musk of the Moose`, 30 | "": $effects`Empathy, Leash of Linguini, Astral Shell, Elemental Saucesphere`, 31 | "fam weight": $effects`Chorale of Companionship`, 32 | init: $effects`Walberg's Dim Bulb, Springy Fusilli`, 33 | ML: $effects`Ur-Kel's Aria of Annoyance, Pride of the Puffin, Drescher's Annoying Noise`, 34 | item: $effects`Fat Leon's Phat Loot Lyric, Singer's Faithful Ocelot`, 35 | meat: $effects`Polka of Plenty`, 36 | mainstat: $effects`Big, Tomato Power, Trivia Master, Gr8ness, Carol of the Hells, Carol of the Thrills`, 37 | muscle: $effects`Go Get 'Em\, Tiger!, Phorcefullness, Incredibly Hulking`, 38 | mysticality: $effects`Glittering Eyelashes, Mystically Oiled, On the Shoulders of Giants`, 39 | moxie: $effects`Butt-Rock Hair, Superhuman Sarcasm, Cock of the Walk`, 40 | }; 41 | 42 | // Class-specific 43 | if (myClass() === $class`Seal Clubber`) result["init"].push($effect`Silent Hunting`); 44 | else result["init"].push($effect`Nearly Silent Hunting`); 45 | 46 | // One-per-day 47 | if (!get("_ballpit")) result["mainstat"].push($effect`Having a Ball!`); 48 | if (!get("_lyleFavored")) result["mainstat"].push($effect`Favored by Lyle`); 49 | if (!get("telescopeLookedHigh")) result["mainstat"].push($effect`Starry-Eyed`); 50 | if (get("spacegateAlways") && get("spacegateVaccine2") && !get("_spacegateVaccine")) 51 | result["mainstat"].push($effect`Broad-Spectrum Vaccine`); 52 | if (have($skill`Emotionally Chipped`) && get("_feelExcitementUsed") < 3) 53 | result["mainstat"].push($effect`Feeling Excited`); 54 | if (have($item`protonic accelerator pack`) && !get("_streamsCrossed")) 55 | result["mainstat"].push($effect`Total Protonic Reversal`); 56 | 57 | // Noncombat buffs 58 | if ( 59 | haveSkill($skill`Feel Lonely`) && 60 | (get("_feelLonelyUsed") < 3 || have($effect`Feeling Lonely`)) 61 | ) 62 | result["-combat"].push($effect`Feeling Lonely`); 63 | if (!get("_olympicSwimmingPool") || have($effect`Silent Running`)) 64 | result["-combat"].push($effect`Silent Running`); 65 | // TODO: Silence of the God Lobster? 66 | // TODO: Snow cleats? 67 | 68 | return result; 69 | } 70 | 71 | function isSong(effect: Effect) { 72 | return toSkill(effect).class === $class`Accordion Thief` && toSkill(effect).buff; 73 | } 74 | 75 | function maxSongs(): number { 76 | return have($skill`Mariachi Memory`) ? 4 : 3; 77 | } 78 | 79 | function shrug(effects: Effect[]) { 80 | for (const effect of effects) { 81 | if (have(effect)) uneffect(effect); 82 | } 83 | } 84 | 85 | export function applyEffects(modifier: string): void { 86 | const relevantEffects = getRelevantEffects(); 87 | 88 | const useful_effects = []; 89 | for (const key in relevantEffects) { 90 | if (modifier.includes(key)) { 91 | useful_effects.push(...relevantEffects[key]); 92 | } 93 | } 94 | 95 | if (modifier.includes("MP")) { 96 | useful_effects.push(...relevantEffects["mysticality"]); 97 | } 98 | 99 | // Handle mainstat buffing and equalizing 100 | switch (myPrimestat()) { 101 | case $stat`Muscle`: 102 | if (modifier.includes("mainstat")) useful_effects.push(...relevantEffects["muscle"]); 103 | if (modifier.includes("moxie") || modifier.includes("myst")) 104 | useful_effects.push($effect`Stabilizing Oiliness`); 105 | break; 106 | case $stat`Mysticality`: 107 | if (modifier.includes("mainstat")) useful_effects.push(...relevantEffects["mysticality"]); 108 | if (modifier.includes("moxie") || modifier.includes("muscle")) 109 | useful_effects.push($effect`Expert Oiliness`); 110 | break; 111 | case $stat`Moxie`: 112 | if (modifier.includes("mainstat")) useful_effects.push(...relevantEffects["moxie"]); 113 | if (modifier.includes("muscle") || modifier.includes("myst")) 114 | useful_effects.push($effect`Slippery Oiliness`); 115 | break; 116 | } 117 | 118 | // Remove wrong combat effects 119 | if (modifier.includes("+combat")) shrug(relevantEffects["-combat"]); 120 | if (modifier.includes("-combat")) shrug(relevantEffects["+combat"]); 121 | 122 | if (myMaxmp() < 27 && have($skill`The Magical Mojomuscular Melody`)) { 123 | useful_effects.unshift($effect`The Magical Mojomuscular Melody`); 124 | } 125 | 126 | // Make room for songs 127 | const songs = []; 128 | for (const effect of useful_effects) { 129 | if (isSong(effect)) songs.push(effect); 130 | } 131 | if (songs.length > maxSongs()) throw "Too many AT songs."; 132 | if (songs.length > 0) { 133 | const extra_songs = []; 134 | for (const effect_name of Object.keys(myEffects())) { 135 | const effect = Effect.get(effect_name); 136 | if (isSong(effect) && !songs.includes(effect)) { 137 | extra_songs.push(effect); 138 | } 139 | } 140 | while (songs.length + extra_songs.length > maxSongs()) { 141 | const to_remove = extra_songs.pop(); 142 | if (to_remove === undefined) break; 143 | else uneffect(to_remove); 144 | } 145 | } 146 | 147 | // Use horsery 148 | if (get("horseryAvailable")) { 149 | if (modifier.includes("-combat") && get("_horsery") !== "dark horse") { 150 | cliExecute("horsery dark"); 151 | } 152 | // TODO: +combat? 153 | } 154 | 155 | // Use asdon martin 156 | if (getWorkshed() === $item`Asdon Martin keyfob`) { 157 | if (modifier.includes("-combat")) AsdonMartin.drive(AsdonMartin.Driving.Stealthily); 158 | else if (modifier.includes("+combat")) AsdonMartin.drive(AsdonMartin.Driving.Obnoxiously); 159 | else if (modifier.includes("init")) AsdonMartin.drive(AsdonMartin.Driving.Quickly); 160 | else if (modifier.includes("item")) AsdonMartin.drive(AsdonMartin.Driving.Observantly); 161 | } 162 | 163 | // Apply all relevant effects 164 | for (const effect of useful_effects) { 165 | ensureEffect(effect); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cliExecute, 3 | gametimeToInt, 4 | Item, 5 | myAdventures, 6 | myClosetMeat, 7 | myLevel, 8 | myMeat, 9 | print, 10 | takeCloset, 11 | turnsPlayed, 12 | } from "kolmafia"; 13 | import { all_tasks, level_tasks, organ_tasks, quest_tasks } from "./tasks/all"; 14 | import { prioritize } from "./route"; 15 | import { Engine } from "./engine/engine"; 16 | import { convertMilliseconds, debug } from "./lib"; 17 | import { $item, $skill, get, have, set, sinceKolmafiaRevision } from "libram"; 18 | import { Task } from "./engine/task"; 19 | import { Args, step } from "grimoire-kolmafia"; 20 | 21 | const worksheds = [ 22 | // eslint-disable-next-line libram/verify-constants 23 | [$item`model train set`, "Swap to model train set"], 24 | [$item`cold medicine cabinet`, "Swap to cold medicine cabinet"], 25 | [$item`Asdon Martin keyfob`, "Swap to asdon martin keyfob"], 26 | ] as [Item, string][]; 27 | 28 | export const args = Args.create("loopcasual", "A script to complete casual runs.", { 29 | goal: Args.string({ 30 | help: "Which tasks to perform.", 31 | options: [ 32 | ["all", "Level up, complete all quests, and get your steel organ."], 33 | ["level", "Level up only."], 34 | ["quests", "Complete all quests only."], 35 | ["organ", "Get your steel organ only."], 36 | ["!organ", "Level up and complete all quests only."], 37 | ], 38 | default: "all", 39 | }), 40 | stomach: Args.number({ 41 | help: "Amount of stomach to fill.", 42 | default: 5, 43 | }), 44 | liver: Args.number({ 45 | help: "Amount of liver to fill.", 46 | default: 10, 47 | }), 48 | spleen: Args.number({ 49 | help: "Amount of spleen to fill.", 50 | default: 5, 51 | }), 52 | voa: Args.number({ 53 | help: "Value of an adventure, in meat, for determining diet.", 54 | setting: "valueOfAdventure", 55 | default: 6500, 56 | }), 57 | actions: Args.number({ 58 | help: "Maximum number of actions to perform, if given. Can be used to execute just a few steps at a time.", 59 | }), 60 | levelto: Args.number({ 61 | help: "Aim to level to this with free leveling resources.", 62 | default: 13, 63 | }), 64 | professor: Args.flag({ 65 | help: "Use pocket professor as one of the free leveling resources. This uses up some copiers, but may help to level.", 66 | default: false, 67 | }), 68 | fluffers: Args.boolean({ 69 | help: "If true, use stuffing fluffers to finish the war.", 70 | default: true, 71 | }), 72 | workshed: Args.item({ 73 | help: "Workshed item to place in an empty workshed at the start of the run.", 74 | // eslint-disable-next-line libram/verify-constants 75 | default: $item`model train set`, 76 | options: worksheds, 77 | }), 78 | }); 79 | export function main(command?: string): void { 80 | sinceKolmafiaRevision(27108); 81 | 82 | Args.fill(args, command); 83 | if (args.help) { 84 | Args.showHelp(args); 85 | return; 86 | } 87 | 88 | if (runComplete()) { 89 | print("Casual complete!", "purple"); 90 | return; 91 | } 92 | 93 | const time_property = "_loop_casual_first_start"; 94 | const set_time_now = get(time_property, -1) === -1; 95 | if (set_time_now) set(time_property, gametimeToInt()); 96 | 97 | if (myMeat() > 2000000) { 98 | print("You have too much meat; closeting some during execution."); 99 | cliExecute(`closet put ${myMeat() - 2000000} meat`); 100 | } 101 | 102 | // Select which tasks to perform 103 | let tasks: Task[] = []; 104 | switch (args.goal) { 105 | case "all": 106 | tasks = prioritize(all_tasks()); 107 | break; 108 | case "level": 109 | tasks = prioritize(level_tasks(), true); 110 | break; 111 | case "quests": 112 | tasks = prioritize(quest_tasks(), true); 113 | break; 114 | case "organ": 115 | tasks = prioritize(organ_tasks(), true); 116 | break; 117 | case "!organ": 118 | tasks = prioritize([...level_tasks(), ...quest_tasks()], true); 119 | break; 120 | } 121 | 122 | const engine = new Engine(tasks); 123 | 124 | try { 125 | let actions_left = args.actions ?? Number.MAX_VALUE; 126 | // eslint-disable-next-line no-constant-condition 127 | while (true) { 128 | // Locate the next task. 129 | const next = engine.getNextTask(); 130 | if (next === undefined) break; 131 | 132 | // Track the number of actions remaining to execute. 133 | // If there are no more actions left, just print our plan and exit. 134 | if (actions_left <= 0) { 135 | debug(`Next task: ${next.name}`); 136 | return; 137 | } else { 138 | actions_left -= 1; 139 | } 140 | 141 | // Do the next task. 142 | engine.execute(next); 143 | } 144 | 145 | // Script is done; ensure we have finished 146 | takeCloset(myClosetMeat()); 147 | 148 | const remaining_tasks = tasks.filter((task) => !task.completed()); 149 | if (!runComplete()) { 150 | debug("Remaining tasks:", "red"); 151 | for (const task of remaining_tasks) { 152 | if (!task.completed()) debug(`${task.name}`, "red"); 153 | } 154 | if (myAdventures() === 0) { 155 | throw `Ran out of adventures. Consider setting higher stomach, liver, and spleen usage, or a higher voa.`; 156 | } else { 157 | throw `Unable to find available task, but the run is not complete.`; 158 | } 159 | } 160 | } finally { 161 | engine.propertyManager.resetAll(); 162 | } 163 | 164 | print("Casual complete!", "purple"); 165 | print(` Adventures used: ${turnsPlayed()}`, "purple"); 166 | print(` Adventures remaining: ${myAdventures()}`, "purple"); 167 | if (set_time_now) 168 | print( 169 | ` Time: ${convertMilliseconds(gametimeToInt() - get(time_property, gametimeToInt()))}`, 170 | "purple" 171 | ); 172 | else 173 | print( 174 | ` Time: ${convertMilliseconds( 175 | gametimeToInt() - get(time_property, gametimeToInt()) 176 | )} since first run today started`, 177 | "purple" 178 | ); 179 | } 180 | 181 | function runComplete(): boolean { 182 | switch (args.goal) { 183 | case "all": 184 | return ( 185 | step("questL13Final") === 999 && have($skill`Liver of Steel`) && myLevel() >= args.levelto 186 | ); 187 | case "level": 188 | return myLevel() >= args.levelto; 189 | case "quests": 190 | return step("questL13Final") === 999; 191 | case "organ": 192 | return have($skill`Liver of Steel`); 193 | case "!organ": 194 | return step("questL13Final") === 999 && myLevel() >= args.levelto; 195 | default: 196 | throw `Unknown goal ${args.goal}`; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/tasks/level9.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, floor, itemAmount, myLevel, use, visitUrl } from "kolmafia"; 2 | import { 3 | $effect, 4 | $effects, 5 | $item, 6 | $items, 7 | $location, 8 | $monster, 9 | $skill, 10 | get, 11 | have, 12 | Macro, 13 | SourceTerminal, 14 | } from "libram"; 15 | import { Quest, Task } from "../engine/task"; 16 | import { CombatStrategy } from "../engine/combat"; 17 | import { OutfitSpec, step } from "grimoire-kolmafia"; 18 | 19 | const ABoo: Task[] = [ 20 | { 21 | name: "ABoo Start", 22 | after: ["Start Peaks"], 23 | completed: () => 24 | $location`A-Boo Peak`.noncombatQueue.includes("Faction Traction = Inaction") || 25 | get("booPeakProgress") < 50, 26 | do: $location`A-Boo Peak`, 27 | limit: { tries: 1 }, 28 | }, 29 | { 30 | name: "ABoo Clues", 31 | after: ["ABoo Start"], 32 | acquire: [ 33 | { item: $item`yellow rocket`, useful: () => !have($effect`Everything Looks Yellow`) }, 34 | ], 35 | priority: () => get("lastCopyableMonster") === $monster`toothy sklelton`, // After Defiled Nook 36 | completed: () => itemAmount($item`A-Boo clue`) * 30 >= get("booPeakProgress"), 37 | prepare: () => { 38 | if (!SourceTerminal.isCurrentSkill($skill`Duplicate`)) 39 | SourceTerminal.educate([$skill`Duplicate`, $skill`Digitize`]); 40 | }, 41 | do: $location`A-Boo Peak`, 42 | outfit: (): OutfitSpec => { 43 | if ( 44 | $location`A-Boo Peak`.turnsSpent === 0 && 45 | $location`Twin Peak`.turnsSpent === 0 && 46 | $location`Oil Peak`.turnsSpent === 0 && 47 | have($skill`Comprehensive Cartography`) 48 | ) { 49 | // Prepare for Ghostly Memories (1430) 50 | return { modifier: "spooky res, cold res, HP" }; 51 | } else { 52 | return { 53 | modifier: "item 667max", 54 | equip: $items`A Light that Never Goes Out`, 55 | skipDefaults: true, 56 | }; 57 | } 58 | }, 59 | effects: $effects`Merry Smithsness`, 60 | combat: new CombatStrategy() 61 | .macro((): Macro => { 62 | if (get("lastCopyableMonster") === $monster`toothy sklelton`) { 63 | return new Macro() 64 | .trySkill($skill`Feel Nostalgic`) 65 | .trySkill(`Duplicate`) 66 | .tryItem(`yellow rocket`); 67 | } else { 68 | return new Macro() 69 | .trySkill($skill`Feel Envy`) 70 | .trySkill($skill`Saucegeyser`) 71 | .repeat(); 72 | } 73 | }) 74 | .killHard(), 75 | choices: { 611: 1, 1430: 1 }, 76 | limit: { tries: 4 }, 77 | }, 78 | { 79 | name: "ABoo Horror", 80 | after: ["ABoo Clues"], 81 | ready: () => have($item`A-Boo clue`), 82 | completed: () => get("booPeakProgress") === 0, 83 | prepare: () => { 84 | use($item`A-Boo clue`); 85 | }, 86 | do: $location`A-Boo Peak`, 87 | outfit: { modifier: "spooky res, cold res, HP" }, 88 | choices: { 611: 1 }, 89 | limit: { tries: 4 }, 90 | }, 91 | { 92 | name: "ABoo Peak", 93 | after: ["ABoo Horror"], 94 | completed: () => get("booPeakLit"), 95 | do: $location`A-Boo Peak`, 96 | limit: { tries: 1 }, 97 | }, 98 | ]; 99 | 100 | const Oil: Task[] = [ 101 | { 102 | name: "Oil Kill", 103 | after: ["Start Peaks"], 104 | completed: () => get("oilPeakProgress") === 0, 105 | do: $location`Oil Peak`, 106 | outfit: { modifier: "ML" }, 107 | combat: new CombatStrategy().kill(), 108 | limit: { tries: 6 }, 109 | }, 110 | { 111 | name: "Oil Peak", 112 | after: ["Oil Kill"], 113 | completed: () => get("oilPeakLit"), 114 | do: $location`Oil Peak`, 115 | limit: { tries: 1 }, 116 | }, 117 | ]; 118 | 119 | const Twin: Task[] = [ 120 | { 121 | name: "Twin Stench", 122 | after: ["Start Peaks"], 123 | priority: () => get("hasAutumnaton"), 124 | completed: () => !!(get("twinPeakProgress") & 1), 125 | do: () => { 126 | use($item`rusty hedge trimmers`); 127 | }, 128 | choices: { 606: 1, 607: 1 }, 129 | acquire: [{ item: $item`rusty hedge trimmers` }], 130 | outfit: { modifier: "stench res 4min" }, 131 | limit: { tries: 1 }, 132 | }, 133 | { 134 | name: "Twin Item", 135 | after: ["Start Peaks"], 136 | completed: () => !!(get("twinPeakProgress") & 2), 137 | do: () => { 138 | use($item`rusty hedge trimmers`); 139 | }, 140 | choices: { 606: 2, 608: 1 }, 141 | acquire: [{ item: $item`rusty hedge trimmers` }], 142 | outfit: { modifier: "item 50min" }, 143 | limit: { tries: 1 }, 144 | }, 145 | { 146 | name: "Twin Oil", 147 | after: ["Start Peaks"], 148 | completed: () => !!(get("twinPeakProgress") & 4), 149 | do: () => { 150 | use($item`rusty hedge trimmers`); 151 | }, 152 | choices: { 606: 3, 609: 1, 616: 1 }, 153 | acquire: [{ item: $item`rusty hedge trimmers` }, { item: $item`jar of oil` }], 154 | limit: { tries: 1 }, 155 | }, 156 | { 157 | name: "Twin Init", 158 | after: ["Twin Stench", "Twin Item", "Twin Oil"], 159 | completed: () => !!(get("twinPeakProgress") & 8), 160 | do: () => { 161 | use($item`rusty hedge trimmers`); 162 | }, 163 | choices: { 606: 4, 610: 1, 1056: 1 }, 164 | acquire: [{ item: $item`rusty hedge trimmers` }], 165 | limit: { tries: 1 }, 166 | }, 167 | ]; 168 | 169 | export const ChasmQuest: Quest = { 170 | name: "Orc Chasm", 171 | tasks: [ 172 | { 173 | name: "Start", 174 | after: ["Toot/Finish"], 175 | priority: () => get("hasAutumnaton"), 176 | ready: () => myLevel() >= 9, 177 | completed: () => step("questL09Topping") !== -1, 178 | do: () => visitUrl("council.php"), 179 | limit: { tries: 1 }, 180 | freeaction: true, 181 | }, 182 | { 183 | name: "Bridge", 184 | after: ["Start"], 185 | priority: () => get("hasAutumnaton"), 186 | completed: () => step("questL09Topping") >= 1, 187 | do: (): void => { 188 | if (have($item`fish hatchet`)) use($item`fish hatchet`); 189 | visitUrl(`place.php?whichplace=orc_chasm&action=bridge${get("chasmBridgeProgress")}`); // use existing materials 190 | const count = floor((34 - get("chasmBridgeProgress")) / 5); 191 | if (count <= 0) return; 192 | cliExecute(`acquire ${count} snow boards`); 193 | visitUrl(`place.php?whichplace=orc_chasm&action=bridge${get("chasmBridgeProgress")}`); 194 | }, 195 | acquire: [{ item: $item`snow berries`, num: 12 }], 196 | limit: { tries: 1 }, 197 | freeaction: true, 198 | }, 199 | { 200 | name: "Start Peaks", 201 | after: ["Bridge"], 202 | priority: () => get("hasAutumnaton"), 203 | completed: () => step("questL09Topping") >= 2, 204 | do: () => visitUrl("place.php?whichplace=highlands&action=highlands_dude"), 205 | limit: { tries: 1 }, 206 | freeaction: true, 207 | }, 208 | ...ABoo, 209 | ...Oil, 210 | ...Twin, 211 | { 212 | name: "Finish", 213 | after: ["ABoo Peak", "Oil Peak", "Twin Init"], 214 | completed: () => step("questL09Topping") === 999, 215 | do: () => visitUrl("place.php?whichplace=highlands&action=highlands_dude"), 216 | limit: { tries: 1 }, 217 | freeaction: true, 218 | }, 219 | ], 220 | }; 221 | -------------------------------------------------------------------------------- /src/tasks/level11.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buy, 3 | cliExecute, 4 | haveEquipped, 5 | itemAmount, 6 | myLevel, 7 | runChoice, 8 | use, 9 | visitUrl, 10 | } from "kolmafia"; 11 | import { 12 | $coinmaster, 13 | $effect, 14 | $familiar, 15 | $item, 16 | $items, 17 | $location, 18 | $monster, 19 | $skill, 20 | get, 21 | have, 22 | Macro, 23 | } from "libram"; 24 | import { Quest, Task } from "../engine/task"; 25 | import { CombatStrategy } from "../engine/combat"; 26 | import { OutfitSpec, step } from "grimoire-kolmafia"; 27 | 28 | const Diary: Task[] = [ 29 | { 30 | name: "Forest", 31 | after: ["Start"], 32 | acquire: [{ item: $item`blackberry galoshes` }], 33 | completed: () => step("questL11Black") >= 2, 34 | do: $location`The Black Forest`, 35 | outfit: { 36 | equip: $items`blackberry galoshes`, 37 | familiar: $familiar`Reassembled Blackbird`, 38 | modifier: "+combat", 39 | }, 40 | choices: { 923: 1, 924: 1 }, 41 | combat: new CombatStrategy().ignore($monster`blackberry bush`).kill(), 42 | limit: { soft: 15 }, 43 | }, 44 | { 45 | name: "Buy Documents", 46 | after: ["Forest"], 47 | completed: () => have($item`forged identification documents`) || step("questL11Black") >= 4, 48 | do: (): void => { 49 | visitUrl("woods.php"); 50 | visitUrl("shop.php?whichshop=blackmarket"); 51 | visitUrl("shop.php?whichshop=blackmarket&action=buyitem&whichrow=281&ajax=1&quantity=1"); 52 | }, 53 | limit: { tries: 1 }, 54 | freeaction: true, 55 | }, 56 | { 57 | name: "Diary", 58 | after: ["Buy Documents", "Misc/Unlock Beach"], 59 | completed: () => step("questL11Black") >= 4, 60 | do: $location`The Shore, Inc. Travel Agency`, 61 | choices: { 793: 1 }, 62 | limit: { tries: 1 }, 63 | }, 64 | ]; 65 | 66 | const Desert: Task[] = [ 67 | { 68 | name: "Scrip", 69 | after: ["Misc/Unlock Beach"], 70 | completed: () => have($item`Shore Inc. Ship Trip Scrip`) || have($item`UV-resistant compass`), 71 | do: $location`The Shore, Inc. Travel Agency`, 72 | choices: { 793: 1 }, 73 | limit: { tries: 1 }, 74 | freeaction: true, 75 | }, 76 | { 77 | name: "Compass", 78 | after: ["Misc/Unlock Beach", "Scrip"], 79 | completed: () => have($item`UV-resistant compass`), 80 | do: () => buy($coinmaster`The Shore, Inc. Gift Shop`, 1, $item`UV-resistant compass`), 81 | limit: { tries: 1 }, 82 | freeaction: true, 83 | }, 84 | { 85 | name: "Desert", 86 | after: ["Diary", "Compass"], 87 | acquire: [ 88 | { item: $item`can of black paint`, useful: () => (get("gnasirProgress") & 2) === 0 }, 89 | { item: $item`killing jar`, useful: () => (get("gnasirProgress") & 4) === 0 }, 90 | { item: $item`drum machine`, useful: () => (get("gnasirProgress") & 16) === 0 }, 91 | ], 92 | completed: () => get("desertExploration") >= 100, 93 | do: $location`The Arid, Extra-Dry Desert`, 94 | outfit: (): OutfitSpec => { 95 | const handItems = $items`survival knife, UV-resistant compass`.filter((it) => have(it)); 96 | if ( 97 | have($item`industrial fire extinguisher`) && 98 | get("_fireExtinguisherCharge") >= 20 && 99 | !get("fireExtinguisherDesertUsed") && 100 | have($effect`Ultrahydrated`) 101 | ) 102 | handItems.unshift($item`industrial fire extinguisher`); 103 | return { 104 | equip: handItems.slice(0, 2), 105 | familiar: $familiar`Melodramedary`, 106 | famequip: $item`dromedary drinking helmet`, 107 | }; 108 | }, 109 | combat: new CombatStrategy() 110 | .macro((): Macro => { 111 | if (have($effect`Ultrahydrated`) && !haveEquipped($item`"I Voted!" sticker`)) 112 | return new Macro().trySkill($skill`Fire Extinguisher: Zone Specific`); 113 | else return new Macro(); 114 | }) 115 | .kill(), 116 | post: (): void => { 117 | if (!$location`The Arid, Extra-Dry Desert`.noncombatQueue.includes("A Sietch in Time")) 118 | return; 119 | if ((get("gnasirProgress") & 16) > 0) return; 120 | if ( 121 | itemAmount($item`worm-riding manual page`) >= 15 || 122 | (get("gnasirProgress") & 2) === 0 || 123 | (get("gnasirProgress") & 4) === 0 124 | ) { 125 | let res = visitUrl("place.php?whichplace=desertbeach&action=db_gnasir"); 126 | while (res.includes("value=2")) { 127 | res = runChoice(2); 128 | } 129 | runChoice(1); 130 | } 131 | cliExecute("use * desert sightseeing pamphlet"); 132 | if (have($item`worm-riding hooks`)) use($item`drum machine`); 133 | }, 134 | limit: { soft: 30 }, 135 | delay: 25, 136 | choices: { 805: 1 }, 137 | }, 138 | ]; 139 | 140 | function rotatePyramid(goal: number): void { 141 | const ratchets = (goal - get("pyramidPosition") + 5) % 5; 142 | const to_buy = 143 | ratchets - itemAmount($item`tomb ratchet`) - itemAmount($item`crumbling wooden wheel`); 144 | if (to_buy > 0) { 145 | buy($item`tomb ratchet`, to_buy); 146 | } 147 | visitUrl("place.php?whichplace=pyramid&action=pyramid_control"); 148 | for (let i = 0; i < ratchets; i++) { 149 | if (have($item`crumbling wooden wheel`)) { 150 | visitUrl("choice.php?whichchoice=929&option=1&pwd"); 151 | } else { 152 | visitUrl("choice.php?whichchoice=929&option=2&pwd"); 153 | } 154 | } 155 | if (get("pyramidPosition") !== goal) throw `Failed to rotate pyramid to ${goal}`; 156 | visitUrl("choice.php?whichchoice=929&option=5&pwd"); 157 | } 158 | 159 | const Pyramid: Task[] = [ 160 | { 161 | name: "Open Pyramid", 162 | after: ["Desert", "Manor/Boss", "Palindome/Boss", "Hidden City/Boss"], 163 | completed: () => step("questL11Pyramid") >= 0, 164 | do: () => visitUrl("place.php?whichplace=desertbeach&action=db_pyramid1"), 165 | limit: { tries: 1 }, 166 | freeaction: true, 167 | }, 168 | { 169 | name: "Upper Chamber", 170 | after: ["Open Pyramid"], 171 | completed: () => step("questL11Pyramid") >= 1, 172 | do: $location`The Upper Chamber`, 173 | outfit: { modifier: "+combat" }, 174 | limit: { turns: 6 }, 175 | }, 176 | { 177 | name: "Middle Chamber", 178 | after: ["Upper Chamber"], 179 | completed: () => get("controlRoomUnlock"), 180 | do: $location`The Middle Chamber`, 181 | limit: { turns: 11 }, 182 | delay: 9, 183 | }, 184 | { 185 | name: "Get Token", 186 | acquire: [{ item: $item`tomb ratchet`, num: 3 }], 187 | after: ["Middle Chamber"], 188 | completed: () => 189 | have($item`ancient bronze token`) || have($item`ancient bomb`) || get("pyramidBombUsed"), 190 | do: () => rotatePyramid(4), 191 | limit: { tries: 1 }, 192 | }, 193 | { 194 | name: "Get Bomb", 195 | acquire: [{ item: $item`tomb ratchet`, num: 4 }], 196 | after: ["Get Token"], 197 | completed: () => have($item`ancient bomb`) || get("pyramidBombUsed"), 198 | do: () => rotatePyramid(3), 199 | limit: { tries: 1 }, 200 | }, 201 | { 202 | name: "Use Bomb", 203 | acquire: [{ item: $item`tomb ratchet`, num: 3 }], 204 | after: ["Get Bomb"], 205 | completed: () => get("pyramidBombUsed"), 206 | do: () => rotatePyramid(1), 207 | limit: { tries: 1 }, 208 | }, 209 | { 210 | name: "Boss", 211 | after: ["Use Bomb"], 212 | completed: () => step("questL11Pyramid") === 999, 213 | do: () => visitUrl("place.php?whichplace=pyramid&action=pyramid_state1a"), 214 | boss: true, 215 | combat: new CombatStrategy() 216 | .macro( 217 | new Macro() 218 | .trySkill($skill`Saucegeyser`) 219 | .attack() 220 | .repeat() 221 | ) 222 | .kill(), 223 | limit: { tries: 1 }, 224 | }, 225 | ]; 226 | 227 | export const MacguffinQuest: Quest = { 228 | name: "Macguffin", 229 | tasks: [ 230 | { 231 | name: "Start", 232 | after: ["Toot/Finish"], 233 | ready: () => myLevel() >= 11, 234 | completed: () => step("questL11MacGuffin") !== -1, 235 | do: () => visitUrl("council.php"), 236 | limit: { tries: 1 }, 237 | freeaction: true, 238 | }, 239 | ...Diary, 240 | ...Desert, 241 | ...Pyramid, 242 | { 243 | name: "Finish", 244 | after: ["Boss"], 245 | completed: () => step("questL11MacGuffin") === 999, 246 | do: () => visitUrl("council.php"), 247 | limit: { tries: 1 }, 248 | freeaction: true, 249 | }, 250 | ], 251 | }; 252 | -------------------------------------------------------------------------------- /src/engine/engine.ts: -------------------------------------------------------------------------------- 1 | import { Location, Monster, myAdventures } from "kolmafia"; 2 | import { Task } from "./task"; 3 | import { 4 | $effect, 5 | $familiar, 6 | $item, 7 | $locations, 8 | $skill, 9 | have, 10 | Macro, 11 | PropertiesManager, 12 | } from "libram"; 13 | import { CombatActions, MyActionDefaults } from "./combat"; 14 | import { Engine as BaseEngine, CombatResources, CombatStrategy, Outfit } from "grimoire-kolmafia"; 15 | import { applyEffects } from "./moods"; 16 | import { myHp, myMaxhp, myMaxmp, restoreMp, useSkill } from "kolmafia"; 17 | import { debug } from "../lib"; 18 | import { 19 | canChargeVoid, 20 | freekillSources, 21 | runawaySources, 22 | unusedBanishes, 23 | WandererSource, 24 | wandererSources, 25 | } from "./resources"; 26 | import { equipDefaults, equipFirst, equipInitial, equipUntilCapped, fixFoldables } from "./outfit"; 27 | import { flyersDone } from "../tasks/level12"; 28 | 29 | type ActiveTask = Task & { 30 | wanderer?: WandererSource; 31 | }; 32 | 33 | export class Engine extends BaseEngine { 34 | constructor(tasks: Task[]) { 35 | super(tasks, { combat_defaults: new MyActionDefaults() }); 36 | } 37 | 38 | public available(task: Task): boolean { 39 | if (myAdventures() === 0 && !task.noadventures) return false; 40 | return super.available(task); 41 | } 42 | 43 | public hasDelay(task: Task): boolean { 44 | if (!task.delay) return false; 45 | if (!(task.do instanceof Location)) return false; 46 | return task.do.turnsSpent < task.delay; 47 | } 48 | 49 | public getNextTask(): ActiveTask | undefined { 50 | // First, check for any prioritized tasks 51 | const priority = this.tasks.find( 52 | (task) => this.available(task) && task.priority !== undefined && task.priority() 53 | ); 54 | if (priority !== undefined) { 55 | return priority; 56 | } 57 | 58 | // If a wanderer is up try to place it in a useful location 59 | const wanderer = wandererSources.find((source) => source.available() && source.chance() === 1); 60 | const delay_burning = this.tasks.find( 61 | (task) => 62 | this.hasDelay(task) && 63 | this.available(task) && 64 | this.createOutfit(task).canEquip(wanderer?.equip ?? []) 65 | ); 66 | if (wanderer !== undefined && delay_burning !== undefined) { 67 | return { ...delay_burning, wanderer: wanderer }; 68 | } 69 | 70 | // Otherwise, just advance the next quest on the route 71 | const todo = this.tasks.find((task) => this.available(task)); 72 | if (todo !== undefined) return todo; 73 | 74 | // No next task 75 | return undefined; 76 | } 77 | 78 | public execute(task: ActiveTask): void { 79 | debug(``); 80 | debug(`Executing ${task.name}`, "blue"); 81 | this.checkLimits({ ...task, limit: { ...task.limit, unready: false } }, () => true); // ignore unready for this initial check 82 | super.execute(task); 83 | if (have($effect`Beaten Up`)) throw "Fight was lost; stop."; 84 | if (task.completed()) { 85 | debug(`${task.name} completed!`, "blue"); 86 | } else { 87 | debug(`${task.name} not completed!`, "blue"); 88 | } 89 | } 90 | 91 | customize( 92 | task: ActiveTask, 93 | outfit: Outfit, 94 | combat: CombatStrategy, 95 | resources: CombatResources 96 | ): void { 97 | equipInitial(outfit); 98 | const wanderers = task.wanderer ? [task.wanderer] : []; 99 | for (const wanderer of wanderers) { 100 | if (!outfit.equip(wanderer?.equip ?? [])) 101 | throw `Wanderer equipment ${wanderer.equip} conflicts with ${task.name}`; 102 | } 103 | 104 | if (task.freeaction) { 105 | // Prepare only as requested by the task 106 | return; 107 | } 108 | 109 | // Prepare combat macro 110 | if (combat.getDefaultAction() === undefined) combat.action("ignore"); 111 | 112 | // Use rock-band flyers if needed (300 extra as a buffer for mafia tracking) 113 | const blacklist = new Set($locations`Oil Peak`); 114 | if ( 115 | have($item`rock band flyers`) && 116 | !flyersDone() && 117 | (!(task.do instanceof Location) || !blacklist.has(task.do)) && 118 | task.name !== "Misc/Protonic Ghost" 119 | ) { 120 | combat.macro( 121 | new Macro().if_( 122 | // Avoid sausage goblin (2104), ninja snowman assassin (1185), protagonist (160), quantum mechanic (223), voting monsters 123 | "!hpbelow 50 && !monsterid 2104 && !monsterid 1185 &&!monsterid 160 && !monsterid 223 && !monsterid 2094 && !monsterid 2095 && !monsterid 2096 && !monsterid 2097 && !monsterid 2098", 124 | new Macro().tryItem($item`rock band flyers`) 125 | ), 126 | undefined, 127 | true 128 | ); 129 | } 130 | 131 | if (wanderers.length === 0) { 132 | // Set up a banish if needed 133 | const banishSources = unusedBanishes( 134 | combat.where("banish").filter((mon) => mon instanceof Monster) 135 | ); 136 | resources.provide("banish", equipFirst(outfit, banishSources)); 137 | 138 | // Set up a runaway if there are combats we do not care about 139 | let runaway = undefined; 140 | if (combat.can("ignore")) { 141 | runaway = equipFirst(outfit, runawaySources); 142 | resources.provide("ignore", runaway); 143 | } 144 | if (combat.can("ignoreNoBanish")) { 145 | if (runaway !== undefined && !runaway.banishes) 146 | resources.provide("ignoreNoBanish", runaway); 147 | else 148 | resources.provide( 149 | "ignoreNoBanish", 150 | equipFirst( 151 | outfit, 152 | runawaySources.filter((source) => !source.banishes) 153 | ) 154 | ); 155 | } 156 | 157 | // Set up a free kill if needed, or if no free kills will ever be needed again 158 | if ( 159 | combat.can("killFree") || 160 | (combat.can("kill") && 161 | !task.boss && 162 | this.tasks.every((t) => t.completed() || !t.combat?.can("killFree"))) 163 | ) { 164 | resources.provide("killFree", equipFirst(outfit, freekillSources)); 165 | } 166 | } 167 | 168 | // Set up more wanderers if delay is needed 169 | if (wanderers.length === 0 && this.hasDelay(task)) 170 | wanderers.push(...equipUntilCapped(outfit, wandererSources)); 171 | 172 | // Prepare full outfit 173 | if (!outfit.skipDefaults) { 174 | if (task.boss) outfit.equip($familiar`Machine Elf`); 175 | const freecombat = task.freecombat || wanderers.find((wanderer) => wanderer.chance() === 1); 176 | if (!task.boss && !freecombat) outfit.equip($item`carnivorous potted plant`); 177 | if ( 178 | canChargeVoid() && 179 | !freecombat && 180 | ((combat.can("kill") && !resources.has("killFree")) || combat.can("killHard") || task.boss) 181 | ) 182 | outfit.equip($item`cursed magnifying glass`); 183 | equipDefaults(outfit); 184 | } 185 | 186 | // Kill wanderers 187 | for (const wanderer of wanderers) { 188 | combat.action("killHard", wanderer.monsters); 189 | if (wanderer.macro) combat.macro(wanderer.macro, wanderer.monsters); 190 | } 191 | if (resources.has("killFree") && !task.boss) { 192 | // Upgrade normal kills to free kills if provided 193 | combat.action( 194 | "killFree", 195 | (combat.where("kill") ?? []).filter((mon) => !mon.boss) 196 | ); 197 | if (combat.getDefaultAction() === "kill") combat.action("killFree"); 198 | } 199 | } 200 | 201 | createOutfit(task: Task): Outfit { 202 | const spec = typeof task.outfit === "function" ? task.outfit() : task.outfit; 203 | const outfit = new Outfit(); 204 | if (spec !== undefined) outfit.equip(spec); // no error on failure 205 | return outfit; 206 | } 207 | 208 | dress(task: Task, outfit: Outfit): void { 209 | outfit.dress(); 210 | fixFoldables(outfit); 211 | applyEffects(outfit.modifier.join(", ")); 212 | 213 | // HP/MP upkeep 214 | if (!task.freeaction) { 215 | if (myHp() < myMaxhp() / 2) useSkill($skill`Cannelloni Cocoon`); 216 | if (!have($effect`Super Skill`)) restoreMp(myMaxmp() < 200 ? myMaxmp() : 200); 217 | } 218 | } 219 | 220 | initPropertiesManager(manager: PropertiesManager): void { 221 | super.initPropertiesManager(manager); 222 | manager.set({ 223 | louvreGoal: 7, 224 | louvreDesiredGoal: 7, 225 | }); 226 | manager.setChoices({ 227 | 1106: 3, // Ghost Dog Chow 228 | 1107: 1, // tennis ball 229 | 1340: 3, // Is There A Doctor In The House? 230 | 1341: 1, // Cure her poison 231 | // June cleaver noncombats 232 | 1467: 1, 233 | 1468: 1, 234 | 1469: 2, 235 | 1470: 2, 236 | 1471: 1, 237 | 1472: 2, 238 | 1473: 2, 239 | 1474: 2, 240 | 1475: 1, 241 | }); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/tasks/level11_manor.ts: -------------------------------------------------------------------------------- 1 | import { create, myClass, myFury, myInebriety, use, useSkill, visitUrl } from "kolmafia"; 2 | import { 3 | $class, 4 | $effect, 5 | $effects, 6 | $item, 7 | $items, 8 | $location, 9 | $monster, 10 | $monsters, 11 | $skill, 12 | ensureEffect, 13 | get, 14 | have, 15 | Macro, 16 | } from "libram"; 17 | import { Quest, Task } from "../engine/task"; 18 | import { CombatStrategy } from "../engine/combat"; 19 | import { step } from "grimoire-kolmafia"; 20 | 21 | const Manor1: Task[] = [ 22 | { 23 | name: "Kitchen", 24 | after: ["Start"], 25 | priority: () => get("hasAutumnaton") && $location`The Haunted Kitchen`.turnsSpent === 0, 26 | completed: () => step("questM20Necklace") >= 1, 27 | do: $location`The Haunted Kitchen`, 28 | outfit: { modifier: "stench res, hot res" }, 29 | choices: { 893: 2 }, 30 | combat: new CombatStrategy().kill(), 31 | limit: { turns: 7 }, 32 | }, 33 | { 34 | name: "Billiards", 35 | after: ["Kitchen"], 36 | completed: () => step("questM20Necklace") >= 3, 37 | prepare: () => { 38 | if (!have($item`government-issued eyeshade`)) ensureEffect($effect`Influence of Sphere`); 39 | }, 40 | acquire: [{ item: $item`T.U.R.D.S. Key`, num: 1, price: 4000, optional: true }], 41 | ready: () => myInebriety() <= 15, // Nonnegative contribution 42 | do: $location`The Haunted Billiards Room`, 43 | choices: { 875: 1, 900: 2, 1436: 2 }, 44 | outfit: () => { 45 | return { 46 | equip: have($item`government-issued eyeshade`) ? $items`government-issued eyeshade` : [], 47 | modifier: "-combat", 48 | }; 49 | }, 50 | combat: new CombatStrategy() 51 | .ignore() 52 | .banish($monster`pooltergeist`) 53 | .macro(new Macro().tryItem($item`T.U.R.D.S. Key`), $monster`chalkdust wraith`) 54 | .kill($monster`pooltergeist (ultra-rare)`), 55 | effects: $effects`Chalky Hand`, 56 | limit: { soft: 10 }, 57 | }, 58 | { 59 | name: "Library", 60 | after: ["Billiards"], 61 | completed: () => step("questM20Necklace") >= 4, 62 | do: $location`The Haunted Library`, 63 | combat: new CombatStrategy() 64 | .banish($monsters`banshee librarian, bookbat`) 65 | .kill($monster`writing desk`), 66 | choices: { 163: 4, 888: 4, 889: 5, 894: 1 }, 67 | limit: { soft: 10 }, 68 | }, 69 | { 70 | name: "Finish Floor1", 71 | after: ["Library"], 72 | completed: () => step("questM20Necklace") === 999, 73 | do: () => visitUrl("place.php?whichplace=manor1&action=manor1_ladys"), 74 | limit: { tries: 1 }, 75 | freeaction: true, 76 | }, 77 | ]; 78 | 79 | const Manor2: Task[] = [ 80 | { 81 | name: "Start Floor2", 82 | after: ["Finish Floor1"], 83 | completed: () => step("questM21Dance") >= 1, 84 | do: () => visitUrl("place.php?whichplace=manor2&action=manor2_ladys"), 85 | limit: { tries: 1 }, 86 | freeaction: true, 87 | }, 88 | { 89 | name: "Gallery Delay", 90 | after: ["Start Floor2"], 91 | completed: () => $location`The Haunted Gallery`.turnsSpent >= 5 || step("questM21Dance") >= 2, 92 | do: $location`The Haunted Gallery`, 93 | choices: { 89: 6, 896: 1 }, // TODO: louvre 94 | limit: { turns: 5 }, 95 | delay: 5, 96 | }, 97 | { 98 | name: "Gallery", 99 | after: ["Gallery Delay"], 100 | completed: () => have($item`Lady Spookyraven's dancing shoes`) || step("questM21Dance") >= 2, 101 | do: $location`The Haunted Gallery`, 102 | choices: { 89: 6, 896: 1 }, // TODO: louvre 103 | outfit: { modifier: "-combat" }, 104 | limit: { soft: 10 }, 105 | }, 106 | { 107 | name: "Bathroom Delay", 108 | after: ["Start Floor2"], 109 | completed: () => $location`The Haunted Bathroom`.turnsSpent >= 5 || step("questM21Dance") >= 2, 110 | do: $location`The Haunted Bathroom`, 111 | choices: { 881: 1, 105: 1, 892: 1 }, 112 | combat: new CombatStrategy().kill($monster`cosmetics wraith`), 113 | limit: { turns: 5 }, 114 | delay: 5, 115 | }, 116 | { 117 | name: "Bathroom", 118 | after: ["Bathroom Delay"], 119 | completed: () => have($item`Lady Spookyraven's powder puff`) || step("questM21Dance") >= 2, 120 | do: $location`The Haunted Bathroom`, 121 | choices: { 881: 1, 105: 1, 892: 1 }, 122 | outfit: { modifier: "-combat" }, 123 | combat: new CombatStrategy().kill($monster`cosmetics wraith`), 124 | limit: { soft: 10 }, 125 | }, 126 | { 127 | name: "Bedroom", 128 | after: ["Start Floor2"], 129 | completed: () => have($item`Lady Spookyraven's finest gown`) || step("questM21Dance") >= 2, 130 | do: $location`The Haunted Bedroom`, 131 | choices: { 876: 1, 877: 1, 878: 3, 879: 1, 880: 1, 897: 2 }, 132 | combat: new CombatStrategy() 133 | .kill($monsters`elegant animated nightstand, animated ornate nightstand`) // kill ornate nightstand if banish fails 134 | .macro( 135 | new Macro().trySkill($skill`Batter Up!`).trySkill($skill`Talk About Politics`), 136 | $monster`animated ornate nightstand` 137 | ) 138 | .banish( 139 | $monsters`animated mahogany nightstand, animated rustic nightstand, Wardröb nightstand` 140 | ) 141 | .ignore($monster`tumbleweed`), 142 | outfit: () => { 143 | if (myClass() === $class`Seal Clubber` && have($skill`Batter Up!`) && myFury() >= 5) 144 | return { equip: $items`Meat Tenderizer is Murder` }; 145 | else return { equip: $items`Pantsgiving` }; 146 | }, 147 | delay: () => (have($item`Lord Spookyraven's spectacles`) ? 5 : 0), 148 | limit: { soft: 10 }, 149 | }, 150 | { 151 | name: "Open Ballroom", 152 | after: ["Gallery", "Bathroom", "Bedroom"], 153 | completed: () => step("questM21Dance") >= 3, 154 | do: () => visitUrl("place.php?whichplace=manor2&action=manor2_ladys"), 155 | limit: { tries: 1 }, 156 | }, 157 | { 158 | name: "Finish Floor2", 159 | after: ["Open Ballroom"], 160 | completed: () => step("questM21Dance") >= 4, 161 | do: $location`The Haunted Ballroom`, 162 | limit: { turns: 1 }, 163 | }, 164 | ]; 165 | 166 | const ManorBasement: Task[] = [ 167 | { 168 | name: "Ballroom Delay", 169 | after: ["Macguffin/Diary", "Finish Floor2"], 170 | completed: () => $location`The Haunted Ballroom`.turnsSpent >= 5 || step("questL11Manor") >= 1, 171 | do: $location`The Haunted Ballroom`, 172 | choices: { 90: 3, 106: 4, 921: 1 }, 173 | limit: { turns: 5 }, 174 | delay: 5, 175 | }, 176 | { 177 | name: "Ballroom", 178 | after: ["Ballroom Delay"], 179 | completed: () => step("questL11Manor") >= 1, 180 | do: $location`The Haunted Ballroom`, 181 | outfit: { modifier: "-combat" }, 182 | choices: { 90: 3, 106: 4, 921: 1 }, 183 | limit: { soft: 10 }, 184 | }, 185 | { 186 | name: "Learn Recipe", 187 | after: ["Ballroom"], 188 | completed: () => get("spookyravenRecipeUsed") === "with_glasses", 189 | do: () => { 190 | visitUrl("place.php?whichplace=manor4&action=manor4_chamberwall"); 191 | use($item`recipe: mortar-dissolving solution`); 192 | }, 193 | outfit: { equip: $items`Lord Spookyraven's spectacles` }, 194 | limit: { tries: 1 }, 195 | }, 196 | { 197 | name: "Wine Cellar", 198 | after: ["Learn Recipe"], 199 | completed: () => 200 | have($item`bottle of Chateau de Vinegar`) || 201 | have($item`unstable fulminate`) || 202 | have($item`wine bomb`) || 203 | step("questL11Manor") >= 3, 204 | priority: () => have($effect`Steely-Eyed Squint`), 205 | prepare: (): void => { 206 | if (!get("_steelyEyedSquintUsed")) useSkill($skill`Steely-Eyed Squint`); 207 | }, 208 | do: $location`The Haunted Wine Cellar`, 209 | outfit: { 210 | equip: $items`A Light that Never Goes Out, Lil' Doctor™ bag`, 211 | modifier: "item, booze drop", 212 | skipDefaults: true, 213 | }, 214 | effects: $effects`Merry Smithsness`, 215 | choices: { 901: 2 }, 216 | combat: new CombatStrategy() 217 | .macro(new Macro().trySkill($skill`Otoscope`), $monster`possessed wine rack`) 218 | .banish($monsters`mad wino, skeletal sommelier`) 219 | .killFree(), 220 | limit: { soft: 10 }, 221 | }, 222 | { 223 | name: "Laundry Room", 224 | after: ["Learn Recipe"], 225 | priority: () => have($effect`Steely-Eyed Squint`), 226 | completed: () => 227 | have($item`blasting soda`) || 228 | have($item`unstable fulminate`) || 229 | have($item`wine bomb`) || 230 | step("questL11Manor") >= 3, 231 | prepare: (): void => { 232 | if (!get("_steelyEyedSquintUsed")) useSkill($skill`Steely-Eyed Squint`); 233 | }, 234 | do: $location`The Haunted Laundry Room`, 235 | outfit: { 236 | equip: $items`A Light that Never Goes Out, Lil' Doctor™ bag`, 237 | modifier: "item, food drop", 238 | skipDefaults: true, 239 | }, 240 | effects: $effects`Merry Smithsness`, 241 | choices: { 891: 2 }, 242 | combat: new CombatStrategy() 243 | .macro( 244 | new Macro().trySkill($skill`Otoscope`).trySkill($skill`Chest X-Ray`), 245 | $monster`cabinet of Dr. Limpieza` 246 | ) 247 | .banish($monsters`plaid ghost, possessed laundry press`) 248 | .killFree(), 249 | limit: { soft: 10 }, 250 | }, 251 | { 252 | name: "Fulminate", 253 | after: ["Wine Cellar", "Laundry Room"], 254 | completed: () => 255 | have($item`unstable fulminate`) || have($item`wine bomb`) || step("questL11Manor") >= 3, 256 | do: () => create($item`unstable fulminate`), 257 | limit: { tries: 1 }, 258 | freeaction: true, 259 | }, 260 | { 261 | name: "Boiler Room", 262 | after: ["Fulminate"], 263 | completed: () => have($item`wine bomb`) || step("questL11Manor") >= 3, 264 | do: $location`The Haunted Boiler Room`, 265 | outfit: { modifier: "ML", equip: $items`unstable fulminate` }, 266 | choices: { 902: 2 }, 267 | combat: new CombatStrategy() 268 | .kill($monster`monstrous boiler`) 269 | .banish($monsters`coaltergeist, steam elemental`), 270 | limit: { soft: 10 }, 271 | }, 272 | { 273 | name: "Blow Wall", 274 | after: ["Boiler Room"], 275 | completed: () => step("questL11Manor") >= 3, 276 | do: () => visitUrl("place.php?whichplace=manor4&action=manor4_chamberwall"), 277 | limit: { tries: 1 }, 278 | freeaction: true, 279 | }, 280 | ]; 281 | 282 | export const ManorQuest: Quest = { 283 | name: "Manor", 284 | tasks: [ 285 | { 286 | name: "Start", 287 | after: [], 288 | completed: () => step("questM20Necklace") >= 0, 289 | do: () => use($item`telegram from Lady Spookyraven`), 290 | limit: { tries: 1 }, 291 | freeaction: true, 292 | }, 293 | ...Manor1, 294 | ...Manor2, 295 | ...ManorBasement, 296 | { 297 | name: "Boss", 298 | after: ["Blow Wall"], 299 | completed: () => step("questL11Manor") >= 999, 300 | do: () => visitUrl("place.php?whichplace=manor4&action=manor4_chamberboss"), 301 | boss: true, 302 | combat: new CombatStrategy().kill(), 303 | limit: { tries: 1 }, 304 | }, 305 | ], 306 | }; 307 | -------------------------------------------------------------------------------- /src/tasks/level11_palindome.ts: -------------------------------------------------------------------------------- 1 | import { create, Item, myHash, runChoice, use, visitUrl } from "kolmafia"; 2 | import { 3 | $effect, 4 | $familiar, 5 | $item, 6 | $items, 7 | $location, 8 | $monster, 9 | $monsters, 10 | $skill, 11 | ensureEffect, 12 | get, 13 | have, 14 | Macro, 15 | uneffect, 16 | } from "libram"; 17 | import { Quest, Task } from "../engine/task"; 18 | import { CombatStrategy } from "../engine/combat"; 19 | import { step } from "grimoire-kolmafia"; 20 | 21 | export function shenItem(item: Item) { 22 | return ( 23 | get("shenQuestItem") === item.name && 24 | (step("questL11Shen") === 1 || step("questL11Shen") === 3 || step("questL11Shen") === 5) 25 | ); 26 | } 27 | 28 | const Copperhead: Task[] = [ 29 | { 30 | name: "Copperhead Start", 31 | after: ["Macguffin/Diary"], 32 | completed: () => step("questL11Shen") >= 1, 33 | do: $location`The Copperhead Club`, 34 | choices: { 1074: 1 }, 35 | limit: { tries: 1 }, 36 | }, 37 | { 38 | name: "Copperhead", 39 | after: ["Copperhead Start"], 40 | ready: () => 41 | step("questL11Shen") === 2 || step("questL11Shen") === 4 || step("questL11Shen") === 6, 42 | completed: () => step("questL11Shen") === 999, 43 | do: $location`The Copperhead Club`, 44 | choices: { 852: 1, 853: 1, 854: 1 }, 45 | limit: { tries: 16 }, 46 | }, 47 | { 48 | name: "Bat Snake", 49 | after: ["Copperhead Start", "Bat/Use Sonar"], 50 | ready: () => shenItem($item`The Stankara Stone`), 51 | completed: () => step("questL11Shen") === 999 || have($item`The Stankara Stone`), 52 | do: $location`The Batrat and Ratbat Burrow`, 53 | combat: new CombatStrategy().killHard($monster`Batsnake`), 54 | limit: { soft: 10 }, 55 | delay: 5, 56 | }, 57 | { 58 | name: "Cold Snake", 59 | after: ["Copperhead Start", "McLargeHuge/Ores"], 60 | ready: () => shenItem($item`The First Pizza`), 61 | completed: () => step("questL11Shen") === 999 || have($item`The First Pizza`), 62 | do: $location`Lair of the Ninja Snowmen`, 63 | combat: new CombatStrategy().killHard($monster`Frozen Solid Snake`).macro((): Macro => { 64 | if (!have($item`li'l ninja costume`)) return new Macro().attack().repeat(); 65 | else return new Macro(); 66 | }), 67 | limit: { soft: 10 }, 68 | delay: 5, 69 | }, 70 | { 71 | name: "Hot Snake Precastle", 72 | after: ["Copperhead Start", "Giant/Ground"], 73 | ready: () => shenItem($item`Murphy's Rancid Black Flag`) && step("questL10Garbage") < 10, 74 | completed: () => step("questL11Shen") === 999 || have($item`Murphy's Rancid Black Flag`), 75 | do: $location`The Castle in the Clouds in the Sky (Top Floor)`, 76 | outfit: { equip: $items`Mohawk wig`, modifier: "-combat" }, 77 | choices: { 675: 4, 676: 4, 677: 4, 678: 1, 679: 1, 1431: 4 }, 78 | combat: new CombatStrategy().killHard($monster`Burning Snake of Fire`), 79 | limit: { soft: 10 }, 80 | delay: 5, 81 | }, 82 | { 83 | name: "Hot Snake Postcastle", 84 | after: ["Copperhead Start", "Giant/Ground"], 85 | ready: () => shenItem($item`Murphy's Rancid Black Flag`) && step("questL10Garbage") >= 10, 86 | completed: () => step("questL11Shen") === 999 || have($item`Murphy's Rancid Black Flag`), 87 | do: $location`The Castle in the Clouds in the Sky (Top Floor)`, 88 | outfit: { modifier: "+combat" }, 89 | combat: new CombatStrategy().killHard($monster`Burning Snake of Fire`), 90 | limit: { soft: 10 }, 91 | delay: 5, 92 | }, 93 | { 94 | name: "Sleaze Star Snake", 95 | after: ["Copperhead Start", "Giant/Unlock HITS"], 96 | ready: () => shenItem($item`The Eye of the Stars`), 97 | completed: () => step("questL11Shen") === 999 || have($item`The Eye of the Stars`), 98 | do: $location`The Hole in the Sky`, 99 | combat: new CombatStrategy().killHard($monster`The Snake With Like Ten Heads`), 100 | limit: { soft: 10 }, 101 | delay: 5, 102 | }, 103 | { 104 | name: "Sleaze Frat Snake", 105 | after: ["Copperhead Start"], 106 | ready: () => shenItem($item`The Lacrosse Stick of Lacoronado`), 107 | completed: () => step("questL11Shen") === 999 || have($item`The Lacrosse Stick of Lacoronado`), 108 | do: $location`The Smut Orc Logging Camp`, 109 | combat: new CombatStrategy().killHard($monster`The Frattlesnake`), 110 | limit: { soft: 10 }, 111 | delay: 5, 112 | }, 113 | { 114 | name: "Spooky Snake Precrypt", 115 | after: ["Copperhead Start"], 116 | ready: () => shenItem($item`The Shield of Brook`) && step("questL07Cyrptic") < 999, 117 | completed: () => step("questL11Shen") === 999 || have($item`The Shield of Brook`), 118 | do: $location`The Unquiet Garves`, 119 | combat: new CombatStrategy().killHard($monster`Snakeleton`), 120 | limit: { soft: 10 }, 121 | delay: 5, 122 | }, 123 | { 124 | name: "Spooky Snake Postcrypt", 125 | after: ["Copperhead Start"], 126 | ready: () => shenItem($item`The Shield of Brook`) && step("questL07Cyrptic") === 999, 127 | completed: () => step("questL11Shen") === 999 || have($item`The Shield of Brook`), 128 | do: $location`The VERY Unquiet Garves`, 129 | combat: new CombatStrategy().killHard($monster`Snakeleton`), 130 | limit: { soft: 10 }, 131 | delay: 5, 132 | }, 133 | ]; 134 | 135 | const Zepplin: Task[] = [ 136 | { 137 | name: "Protesters Start", 138 | after: ["Macguffin/Diary"], 139 | completed: () => step("questL11Ron") >= 1, 140 | do: $location`A Mob of Zeppelin Protesters`, 141 | combat: new CombatStrategy().killHard($monster`The Nuge`), 142 | choices: { 856: 1, 857: 1, 858: 1, 866: 2, 1432: 1 }, 143 | limit: { tries: 1 }, 144 | freeaction: true, 145 | }, 146 | { 147 | name: "Protesters", 148 | after: ["Protesters Start"], 149 | completed: () => get("zeppelinProtestors") >= 80, 150 | acquire: [{ item: $item`11-leaf clover` }], 151 | prepare: (): void => { 152 | if (get("zeppelinProtestors") < 80) { 153 | if (have($skill`Bend Hell`) && !get("_bendHellUsed")) ensureEffect($effect`Bendin' Hell`); 154 | use($item`11-leaf clover`); 155 | } 156 | }, 157 | do: $location`A Mob of Zeppelin Protesters`, 158 | combat: new CombatStrategy().killHard($monster`The Nuge`), 159 | choices: { 856: 1, 857: 1, 858: 1, 866: 2, 1432: 1 }, 160 | outfit: { modifier: "sleaze dmg, sleaze spell dmg", familiar: $familiar`Left-Hand Man` }, 161 | freeaction: true, // fully maximize outfit 162 | limit: { tries: 5, message: "Maybe your available sleaze damage is too low." }, 163 | }, 164 | { 165 | name: "Protesters Finish", 166 | after: ["Protesters"], 167 | completed: () => step("questL11Ron") >= 2, 168 | do: $location`A Mob of Zeppelin Protesters`, 169 | combat: new CombatStrategy().killHard($monster`The Nuge`), 170 | choices: { 856: 1, 857: 1, 858: 1, 866: 2, 1432: 1 }, 171 | limit: { tries: 2 }, // If clovers were used before the intro adventure, we need to clear both the intro and closing advs here. 172 | freeaction: true, 173 | }, 174 | { 175 | name: "Zepplin", 176 | after: ["Protesters Finish"], 177 | acquire: [ 178 | { item: $item`glark cable`, useful: () => get("_glarkCableUses") < 5 }, 179 | { item: $item`Red Zeppelin ticket` }, 180 | ], 181 | completed: () => step("questL11Ron") >= 5, 182 | do: $location`The Red Zeppelin`, 183 | combat: new CombatStrategy() 184 | .kill($monster`Ron "The Weasel" Copperhead`) 185 | .macro((): Macro => { 186 | if (get("_glarkCableUses") < 5) return new Macro().tryItem($item`glark cable`); 187 | else return new Macro(); 188 | }, $monsters`man with the red buttons, red skeleton, red butler, Red Fox`) 189 | .banish($monsters`Red Herring, Red Snapper`) 190 | .kill($monsters`man with the red buttons, red skeleton, red butler, Red Fox`), 191 | limit: { soft: 12 }, 192 | }, 193 | ]; 194 | 195 | const Dome: Task[] = [ 196 | { 197 | name: "Talisman", 198 | after: [ 199 | "Copperhead", 200 | "Zepplin", 201 | "Bat Snake", 202 | "Cold Snake", 203 | "Hot Snake Precastle", 204 | "Hot Snake Postcastle", 205 | ], 206 | completed: () => have($item`Talisman o' Namsilat`), 207 | do: () => create($item`Talisman o' Namsilat`), 208 | limit: { tries: 1 }, 209 | freeaction: true, 210 | }, 211 | { 212 | name: "Palindome Dog", 213 | after: ["Talisman"], 214 | acquire: [{ item: $item`disposable instant camera` }], 215 | completed: () => have($item`photograph of a dog`) || step("questL11Palindome") >= 3, 216 | do: $location`Inside the Palindome`, 217 | outfit: { equip: $items`Talisman o' Namsilat`, modifier: "-combat" }, 218 | combat: new CombatStrategy() 219 | .banish($monsters`Evil Olive, Flock of Stab-bats, Taco Cat, Tan Gnat`) 220 | .macro( 221 | new Macro().item($item`disposable instant camera`), 222 | $monsters`Bob Racecar, Racecar Bob` 223 | ) 224 | .kill($monsters`Bob Racecar, Racecar Bob, Drab Bard, Remarkable Elba Kramer`), 225 | limit: { soft: 20 }, 226 | }, 227 | { 228 | name: "Palindome Dudes", 229 | after: ["Palindome Dog"], 230 | completed: () => have(Item.get(7262)) || step("questL11Palindome") >= 3, 231 | do: $location`Inside the Palindome`, 232 | outfit: { equip: $items`Talisman o' Namsilat`, modifier: "-combat" }, 233 | combat: new CombatStrategy() 234 | .banish($monsters`Evil Olive, Flock of Stab-bats, Taco Cat, Tan Gnat`) 235 | .kill($monsters`Bob Racecar, Racecar Bob, Drab Bard, Remarkable Elba Kramer`), 236 | limit: { soft: 20 }, 237 | }, 238 | { 239 | name: "Palindome Photos", 240 | after: ["Palindome Dudes"], 241 | completed: () => 242 | (have($item`photograph of a red nugget`) && 243 | have($item`photograph of God`) && 244 | have($item`photograph of an ostrich egg`)) || 245 | step("questL11Palindome") >= 3, 246 | do: $location`Inside the Palindome`, 247 | outfit: { equip: $items`Talisman o' Namsilat`, modifier: "-combat" }, 248 | limit: { soft: 20 }, 249 | }, 250 | { 251 | name: "Alarm Gem", 252 | after: ["Palindome Photos"], 253 | completed: () => step("questL11Palindome") >= 3, 254 | do: () => { 255 | if (have(Item.get(7262))) use(Item.get(7262)); 256 | visitUrl("place.php?whichplace=palindome&action=pal_droffice"); 257 | visitUrl( 258 | `choice.php?pwd=${myHash()}&whichchoice=872&option=1&photo1=2259&photo2=7264&photo3=7263&photo4=7265` 259 | ); 260 | use(1, Item.get(7270)); 261 | visitUrl("place.php?whichplace=palindome&action=pal_mroffice"); 262 | visitUrl("clan_viplounge.php?action=hottub"); 263 | uneffect($effect`Beaten Up`); 264 | }, 265 | outfit: { equip: $items`Talisman o' Namsilat` }, 266 | limit: { tries: 1 }, 267 | freeaction: true, 268 | }, 269 | { 270 | name: "Open Alarm", 271 | after: ["Alarm Gem"], 272 | completed: () => step("questL11Palindome") >= 5, 273 | do: () => { 274 | if (!have($item`wet stunt nut stew`)) create($item`wet stunt nut stew`); 275 | visitUrl("place.php?whichplace=palindome&action=pal_mrlabel"); 276 | }, 277 | outfit: { equip: $items`Talisman o' Namsilat` }, 278 | limit: { tries: 1 }, 279 | freeaction: true, 280 | }, 281 | ]; 282 | 283 | export const PalindomeQuest: Quest = { 284 | name: "Palindome", 285 | tasks: [ 286 | ...Copperhead, 287 | ...Zepplin, 288 | ...Dome, 289 | { 290 | name: "Boss", 291 | after: ["Open Alarm"], 292 | completed: () => step("questL11Palindome") === 999, 293 | do: (): void => { 294 | visitUrl("place.php?whichplace=palindome&action=pal_drlabel"); 295 | runChoice(-1); 296 | }, 297 | outfit: { equip: $items`Talisman o' Namsilat, Mega Gem` }, 298 | choices: { 131: 1 }, 299 | boss: true, 300 | combat: new CombatStrategy().kill(), 301 | limit: { tries: 1 }, 302 | }, 303 | ], 304 | }; 305 | -------------------------------------------------------------------------------- /src/tasks/level7.ts: -------------------------------------------------------------------------------- 1 | import { 2 | adv1, 3 | cliExecute, 4 | initiativeModifier, 5 | Item, 6 | myLevel, 7 | runChoice, 8 | toUrl, 9 | useSkill, 10 | visitUrl, 11 | } from "kolmafia"; 12 | import { 13 | $effect, 14 | $familiar, 15 | $item, 16 | $items, 17 | $location, 18 | $monster, 19 | $monsters, 20 | $skill, 21 | ensureEffect, 22 | get, 23 | have, 24 | Macro, 25 | } from "libram"; 26 | import { Quest, Task } from "../engine/task"; 27 | import { CombatStrategy } from "../engine/combat"; 28 | import { OutfitSpec, step } from "grimoire-kolmafia"; 29 | 30 | function tuneCape(): void { 31 | if ( 32 | have($item`unwrapped knock-off retro superhero cape`) && 33 | (get("retroCapeSuperhero") !== "vampire" || get("retroCapeWashingInstructions") !== "kill") 34 | ) { 35 | cliExecute("retrocape vampire kill"); 36 | } 37 | } 38 | 39 | function tryCape(sword: Item, ...rest: Item[]) { 40 | if (have($item`unwrapped knock-off retro superhero cape`)) { 41 | rest.unshift($item`unwrapped knock-off retro superhero cape`); 42 | rest.unshift(sword); 43 | } 44 | return rest; 45 | } 46 | 47 | function farmingNookWithAutumnaton() { 48 | /* If we have fallbot, we want to start the Nook ASAP, but we also don't want to finish it 49 | until we've completed all of the other quests so we don't waste any evil eyes. 50 | 51 | This function returns true if the nook is available to farm and we still have other quests to complete. 52 | */ 53 | return ( 54 | get("hasAutumnaton") && 55 | get("cyrptNookEvilness") < 50 && 56 | !( 57 | step("questL02Larva") === 999 && 58 | step("questL03Rat") === 999 && 59 | step("questL04Bat") === 999 && 60 | step("questL05Goblin") === 999 && 61 | step("questL06Friar") === 999 && 62 | get("cyrptAlcoveEvilness") === 0 && 63 | get("cyrptCrannyEvilness") === 0 && 64 | get("cyrptNicheEvilness") === 0 && 65 | step("questL08Trapper") === 999 && 66 | step("questL09Topping") === 999 && 67 | step("questL10Garbage") === 999 && 68 | step("questL11MacGuffin") === 999 && 69 | step("questL12War") === 999 70 | ) 71 | ); 72 | } 73 | 74 | const slay_macro = new Macro() 75 | .trySkill($skill`Slay the Dead`) 76 | .attack() 77 | .repeat(); 78 | 79 | const Alcove: Task[] = [ 80 | { 81 | name: "Alcove", 82 | after: ["Start"], 83 | prepare: (): void => { 84 | tuneCape(); 85 | // Potions to be used if cheap 86 | if (have($item`ear candle`) && initiativeModifier() < 850) 87 | ensureEffect($effect`Clear Ears, Can't Lose`); 88 | if (have($item`panty raider camouflage`) && initiativeModifier() < 850) 89 | ensureEffect($effect`Hiding in Plain Sight`); 90 | if (have($item`Freddie's blessing of Mercury`) && initiativeModifier() < 850) 91 | ensureEffect($effect`You're High as a Crow, Marty`); 92 | }, 93 | acquire: [ 94 | { item: $item`gravy boat` }, 95 | // Init boosters 96 | { item: $item`ear candle`, price: 2000, optional: true }, 97 | { item: $item`panty raider camouflage`, price: 2000, optional: true }, 98 | { item: $item`Freddie's blessing of Mercury`, price: 2000, optional: true }, 99 | ], 100 | completed: () => get("cyrptAlcoveEvilness") <= 13, 101 | do: $location`The Defiled Alcove`, 102 | outfit: (): OutfitSpec => { 103 | return { 104 | equip: tryCape($item`costume sword`, $item`gravy boat`), 105 | modifier: "init 850max, sword", 106 | familiar: $familiar`Oily Woim`, 107 | }; 108 | }, 109 | choices: { 153: 4 }, 110 | combat: new CombatStrategy().macro(slay_macro, $monsters`modern zmobie, conjoined zmombie`), 111 | limit: { turns: 37 }, 112 | }, 113 | { 114 | name: "Alcove Boss", 115 | after: ["Alcove"], 116 | completed: () => get("cyrptAlcoveEvilness") === 0, 117 | do: $location`The Defiled Alcove`, 118 | boss: true, 119 | combat: new CombatStrategy().kill(), 120 | limit: { tries: 1 }, 121 | }, 122 | ]; 123 | 124 | const Cranny: Task[] = [ 125 | { 126 | name: "Cranny", 127 | after: ["Start"], 128 | prepare: tuneCape, 129 | acquire: [{ item: $item`gravy boat` }], 130 | completed: () => get("cyrptCrannyEvilness") <= 13, 131 | do: $location`The Defiled Cranny`, 132 | outfit: (): OutfitSpec => { 133 | return { 134 | equip: tryCape($item`serpentine sword`, $item`gravy boat`), 135 | modifier: "-combat, ML, sword", 136 | }; 137 | }, 138 | choices: { 523: 4 }, 139 | combat: new CombatStrategy() 140 | .macro( 141 | new Macro() 142 | .trySkill($skill`Slay the Dead`) 143 | .skill($skill`Saucegeyser`) 144 | .repeat(), 145 | $monsters`swarm of ghuol whelps, big swarm of ghuol whelps, giant swarm of ghuol whelps, huge ghuol` 146 | ) 147 | .macro(slay_macro), 148 | limit: { turns: 37 }, 149 | }, 150 | { 151 | name: "Cranny Boss", 152 | after: ["Cranny"], 153 | completed: () => get("cyrptCrannyEvilness") === 0, 154 | do: $location`The Defiled Cranny`, 155 | boss: true, 156 | combat: new CombatStrategy().killHard(), 157 | limit: { tries: 1 }, 158 | }, 159 | ]; 160 | 161 | const Niche: Task[] = [ 162 | { 163 | name: "Niche", 164 | after: ["Start"], 165 | prepare: tuneCape, 166 | acquire: [{ item: $item`gravy boat` }], 167 | completed: () => get("cyrptNicheEvilness") <= 13, 168 | do: $location`The Defiled Niche`, 169 | choices: { 157: 4 }, 170 | outfit: (): OutfitSpec => { 171 | if ( 172 | have($item`industrial fire extinguisher`) && 173 | get("_fireExtinguisherCharge") >= 20 && 174 | !get("fireExtinguisherCyrptUsed") 175 | ) 176 | return { equip: $items`industrial fire extinguisher, gravy boat` }; 177 | else 178 | return { 179 | equip: tryCape($item`serpentine sword`, $item`gravy boat`), 180 | }; 181 | }, 182 | combat: new CombatStrategy() 183 | .macro(new Macro().trySkill($skill`Fire Extinguisher: Zone Specific`).step(slay_macro)) 184 | .banish($monsters`basic lihc, senile lihc, slick lihc`), 185 | limit: { turns: 37 }, 186 | }, 187 | { 188 | name: "Niche Boss", 189 | after: ["Niche"], 190 | completed: () => get("cyrptNicheEvilness") === 0, 191 | do: $location`The Defiled Niche`, 192 | boss: true, 193 | combat: new CombatStrategy().kill(), 194 | limit: { tries: 1 }, 195 | }, 196 | ]; 197 | 198 | const Nook: Task[] = [ 199 | { 200 | name: "Nook", 201 | after: ["Start"], 202 | priority: () => get("lastCopyableMonster") === $monster`spiny skelelton`, 203 | prepare: tuneCape, 204 | acquire: [{ item: $item`gravy boat` }], 205 | ready: () => 206 | (get("camelSpit") >= 100 || !have($familiar`Melodramedary`)) && !farmingNookWithAutumnaton(), 207 | completed: () => get("cyrptNookEvilness") <= 13, 208 | do: (): void => { 209 | useSkill($skill`Map the Monsters`); 210 | if (get("mappingMonsters")) { 211 | visitUrl(toUrl($location`The Defiled Nook`)); 212 | if (get("lastCopyableMonster") === $monster`spiny skelelton`) { 213 | runChoice(1, "heyscriptswhatsupwinkwink=186"); // toothy skeleton 214 | } else { 215 | runChoice(1, "heyscriptswhatsupwinkwink=185"); // spiny skelelton 216 | } 217 | } else { 218 | adv1($location`The Defiled Nook`, 0, ""); 219 | } 220 | }, 221 | post: (): void => { 222 | while (have($item`evil eye`) && get("cyrptNookEvilness") > 25) cliExecute("use * evil eye"); 223 | }, 224 | outfit: (): OutfitSpec => { 225 | return { 226 | equip: tryCape($item`costume sword`, $item`gravy boat`), 227 | modifier: "item 500max", 228 | familiar: $familiar`Melodramedary`, 229 | skipDefaults: true, 230 | }; 231 | }, 232 | choices: { 155: 5, 1429: 1 }, 233 | combat: new CombatStrategy() 234 | .macro((): Macro => { 235 | if (get("lastCopyableMonster") === $monster`party skelteon`) 236 | return new Macro() 237 | .trySkill($skill`Feel Nostalgic`) 238 | .trySkill($skill`%fn, spit on them!`) 239 | .trySkill($skill`Feel Envy`) 240 | .step(slay_macro); 241 | else return new Macro().trySkill($skill`Feel Envy`).step(slay_macro); 242 | }, $monster`spiny skelelton`) 243 | .macro((): Macro => { 244 | if (get("lastCopyableMonster") === $monster`spiny skelelton`) 245 | return new Macro() 246 | .trySkill($skill`Feel Nostalgic`) 247 | .trySkill($skill`%fn, spit on them!`) 248 | .trySkill($skill`Feel Envy`) 249 | .step(slay_macro); 250 | else return new Macro().trySkill($skill`Feel Envy`).step(slay_macro); 251 | }, $monster`toothy sklelton`) 252 | .banish($monster`party skelteon`), 253 | limit: { tries: 3 }, 254 | }, 255 | { 256 | name: "Nook Eye", // In case we get eyes from outside sources (Nostalgia) 257 | after: ["Start"], 258 | ready: () => have($item`evil eye`), 259 | completed: () => get("cyrptNookEvilness") <= 13, 260 | do: (): void => { 261 | cliExecute("use * evil eye"); 262 | }, 263 | freeaction: true, 264 | limit: { tries: 9 }, 265 | }, 266 | { 267 | name: "Nook Simple", 268 | after: ["Start"], 269 | prepare: tuneCape, 270 | acquire: [{ item: $item`gravy boat` }], 271 | priority: () => get("hasAutumnaton"), 272 | ready: () => 273 | (get("cyrptNookEvilness") < 30 || get("hasAutumnaton")) && 274 | !have($item`evil eye`) && 275 | !farmingNookWithAutumnaton(), 276 | completed: () => get("cyrptNookEvilness") <= 13, 277 | do: $location`The Defiled Nook`, 278 | post: (): void => { 279 | while (have($item`evil eye`) && get("cyrptNookEvilness") > 13) cliExecute("use * evil eye"); 280 | }, 281 | outfit: (): OutfitSpec => { 282 | return { 283 | equip: tryCape($item`costume sword`, $item`gravy boat`), 284 | modifier: "item 500max", 285 | }; 286 | }, 287 | choices: { 155: 5, 1429: 1 }, 288 | combat: new CombatStrategy() 289 | .macro(slay_macro, $monsters`spiny skelelton, toothy sklelton`) 290 | .banish($monster`party skelteon`), 291 | limit: { tries: 9 }, 292 | }, 293 | { 294 | name: "Nook Boss", 295 | after: ["Nook", "Nook Eye", "Nook Simple"], 296 | ready: () => !farmingNookWithAutumnaton(), 297 | completed: () => get("cyrptNookEvilness") === 0, 298 | do: $location`The Defiled Nook`, 299 | boss: true, 300 | combat: new CombatStrategy().kill(), 301 | limit: { tries: 1 }, 302 | }, 303 | ]; 304 | 305 | export const CryptQuest: Quest = { 306 | name: "Crypt", 307 | tasks: [ 308 | { 309 | name: "Start", 310 | after: ["Toot/Finish"], 311 | ready: () => myLevel() >= 7, 312 | priority: () => get("hasAutumnaton"), 313 | completed: () => step("questL07Cyrptic") !== -1, 314 | do: () => visitUrl("council.php"), 315 | limit: { tries: 1 }, 316 | freeaction: true, 317 | }, 318 | ...Alcove, 319 | ...Cranny, 320 | ...Niche, 321 | ...Nook, 322 | { 323 | name: "Bonerdagon", 324 | after: ["Alcove Boss", "Cranny Boss", "Niche Boss", "Nook Boss"], 325 | completed: () => step("questL07Cyrptic") >= 1, 326 | do: () => { 327 | adv1($location`Haert of the Cyrpt`, -1, ""); 328 | if (get("lastEncounter") !== "The Bonerdagon") 329 | visitUrl(toUrl($location`The Defiled Cranny`)); 330 | }, 331 | choices: { 527: 1 }, 332 | boss: true, 333 | combat: new CombatStrategy().kill(), 334 | limit: { tries: 2 }, 335 | }, 336 | { 337 | name: "Finish", 338 | after: ["Bonerdagon"], 339 | completed: () => step("questL07Cyrptic") === 999, 340 | do: () => visitUrl("council.php"), 341 | limit: { tries: 1 }, 342 | freeaction: true, 343 | }, 344 | ], 345 | }; 346 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/tasks/leveling.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cliExecute, 3 | myHp, 4 | myLevel, 5 | myMaxhp, 6 | myPrimestat, 7 | runChoice, 8 | runCombat, 9 | totalFreeRests, 10 | useSkill, 11 | visitUrl, 12 | } from "kolmafia"; 13 | import { 14 | $effect, 15 | $effects, 16 | $familiar, 17 | $item, 18 | $items, 19 | $location, 20 | $monster, 21 | $skill, 22 | $stat, 23 | ChateauMantegna, 24 | ensureEffect, 25 | get, 26 | getKramcoWandererChance, 27 | have, 28 | Macro, 29 | set, 30 | Witchess, 31 | } from "libram"; 32 | import { Quest } from "../engine/task"; 33 | import { CombatStrategy } from "../engine/combat"; 34 | import { args } from "../main"; 35 | 36 | function primestatId(): number { 37 | switch (myPrimestat()) { 38 | case $stat`Muscle`: 39 | return 1; 40 | case $stat`Mysticality`: 41 | return 2; 42 | case $stat`Moxie`: 43 | return 3; 44 | } 45 | return 1; 46 | } 47 | 48 | export const LevelingQuest: Quest = { 49 | name: "Leveling", 50 | tasks: [ 51 | { 52 | name: "Cloud Talk", 53 | after: [], 54 | ready: () => get("getawayCampsiteUnlocked"), 55 | completed: () => 56 | have($effect`That's Just Cloud-Talk, Man`) || 57 | get("_campAwayCloudBuffs", 0) > 0 || 58 | myLevel() >= args.levelto, 59 | do: () => visitUrl("place.php?whichplace=campaway&action=campaway_sky"), 60 | freeaction: true, 61 | limit: { tries: 1 }, 62 | }, 63 | { 64 | name: "Daycare", 65 | after: [], 66 | ready: () => get("daycareOpen"), 67 | completed: () => get("_daycareGymScavenges") !== 0 || myLevel() >= args.levelto, 68 | do: (): void => { 69 | if ((get("daycareOpen") || get("_daycareToday")) && !get("_daycareSpa")) { 70 | switch (myPrimestat()) { 71 | case $stat`Muscle`: 72 | cliExecute("daycare muscle"); 73 | break; 74 | case $stat`Mysticality`: 75 | cliExecute("daycare myst"); 76 | break; 77 | case $stat`Moxie`: 78 | cliExecute("daycare moxie"); 79 | break; 80 | } 81 | } 82 | visitUrl("place.php?whichplace=town_wrong&action=townwrong_boxingdaycare"); 83 | runChoice(3); 84 | runChoice(2); 85 | }, 86 | limit: { tries: 1 }, 87 | freeaction: true, 88 | }, 89 | { 90 | name: "Bastille", 91 | after: [], 92 | ready: () => have($item`Bastille Battalion control rig`), 93 | completed: () => get("_bastilleGames") !== 0 || myLevel() >= args.levelto, 94 | do: () => 95 | cliExecute(`bastille ${myPrimestat() === $stat`Mysticality` ? "myst" : myPrimestat()}`), 96 | limit: { tries: 1 }, 97 | freeaction: true, 98 | outfit: { 99 | modifier: "exp", 100 | }, 101 | }, 102 | { 103 | name: "Chateau", 104 | after: [], 105 | ready: () => ChateauMantegna.have(), 106 | completed: () => get("timesRested") >= totalFreeRests() || myLevel() >= args.levelto, 107 | prepare: (): void => { 108 | // Set the chateau to give the proper stats 109 | if (myPrimestat() === $stat`Muscle`) { 110 | ChateauMantegna.changeNightstand("electric muscle stimulator"); 111 | } else if (myPrimestat() === $stat`Mysticality`) { 112 | ChateauMantegna.changeNightstand("foreign language tapes"); 113 | } else if (myPrimestat() === $stat`Moxie`) { 114 | ChateauMantegna.changeNightstand("bowl of potpourri"); 115 | } 116 | 117 | // Set extra free rests 118 | if (ChateauMantegna.getCeiling() !== "ceiling fan") { 119 | ChateauMantegna.changeCeiling("ceiling fan"); 120 | } 121 | }, 122 | do: () => visitUrl("place.php?whichplace=chateau&action=chateau_restbox"), 123 | freeaction: true, 124 | outfit: { 125 | modifier: "exp", 126 | }, 127 | limit: { soft: 40 }, 128 | }, 129 | { 130 | name: "LOV Tunnel", 131 | after: [], 132 | ready: () => get("loveTunnelAvailable"), 133 | completed: () => get("_loveTunnelUsed") || myLevel() >= args.levelto, 134 | do: $location`The Tunnel of L.O.V.E.`, 135 | choices: { 1222: 1, 1223: 1, 1224: primestatId(), 1225: 1, 1226: 2, 1227: 1, 1228: 3 }, 136 | combat: new CombatStrategy() 137 | .macro(() => 138 | new Macro().externalIf( 139 | myPrimestat() === $stat`mysticality`, 140 | new Macro().skill($skill`Saucestorm`).repeat() 141 | ) 142 | ) 143 | .killHard(), 144 | outfit: { 145 | modifier: "mainstat, 4exp", 146 | equip: $items`makeshift garbage shirt`, 147 | familiar: $familiar`Galloping Grill`, 148 | }, 149 | limit: { tries: 1 }, 150 | freecombat: true, 151 | }, 152 | { 153 | name: "Snojo", 154 | after: [], 155 | ready: () => get("snojoAvailable"), 156 | prepare: (): void => { 157 | if (get("snojoSetting") === null) { 158 | visitUrl("place.php?whichplace=snojo&action=snojo_controller"); 159 | runChoice(primestatId()); 160 | } 161 | if (have($item`Greatest American Pants`)) { 162 | ensureEffect($effect`Super Skill`); // after GAP are equipped 163 | } 164 | cliExecute("uneffect ode to booze"); 165 | if (myHp() < myMaxhp()) useSkill($skill`Cannelloni Cocoon`); 166 | }, 167 | completed: () => get("_snojoFreeFights") >= 10 || myLevel() >= 13, 168 | do: $location`The X-32-F Combat Training Snowman`, 169 | post: (): void => { 170 | if (get("_snojoFreeFights") === 10) cliExecute("hottub"); // Clean -stat effects 171 | }, 172 | combat: new CombatStrategy() 173 | .macro((): Macro => { 174 | if (have($familiar`Frumious Bandersnatch`) && have($item`Greatest American Pants`)) { 175 | // Grind exp for Bandersnatch 176 | return new Macro() 177 | .skill($skill`Curse of Weaksauce`) 178 | .skill($skill`Stuffed Mortar Shell`) 179 | .while_("!pastround 27 && !hpbelow 100", new Macro().skill($skill`Cannelloni Cannon`)) 180 | .trySkill($skill`Saucegeyser`) 181 | .attack() 182 | .repeat(); 183 | } else { 184 | // no need to grind exp 185 | return new Macro().skill($skill`Saucegeyser`).repeat(); 186 | } 187 | }) 188 | .killHard(), 189 | outfit: { 190 | familiar: $familiar`Frumious Bandersnatch`, 191 | equip: $items`Greatest American Pants, familiar scrapbook`, 192 | modifier: "mainstat, 4exp, HP", 193 | }, 194 | effects: $effects`Spirit of Peppermint`, 195 | limit: { tries: 10 }, 196 | freecombat: true, 197 | }, 198 | { 199 | name: "God Lobster", 200 | after: [], 201 | acquire: [ 202 | { 203 | item: $item`makeshift garbage shirt`, 204 | get: () => cliExecute("fold makeshift garbage shirt"), 205 | }, 206 | ], 207 | ready: () => have($familiar`God Lobster`), 208 | completed: () => get("_godLobsterFights") >= 3 || myLevel() >= args.levelto, 209 | do: (): void => { 210 | visitUrl("main.php?fightgodlobster=1"); 211 | runCombat(); 212 | runChoice(3); 213 | }, 214 | combat: new CombatStrategy().killHard(), 215 | outfit: { 216 | modifier: "mainstat, 4exp, monster level percent", 217 | equip: $items`makeshift garbage shirt, unbreakable umbrella`, 218 | familiar: $familiar`God Lobster`, 219 | }, 220 | limit: { tries: 3 }, 221 | freecombat: true, 222 | }, 223 | { 224 | name: "Witchess", 225 | after: [], 226 | ready: () => Witchess.have(), 227 | completed: () => Witchess.fightsDone() >= 5 || myLevel() >= args.levelto, 228 | do: () => Witchess.fightPiece($monster`Witchess Knight`), 229 | combat: new CombatStrategy().killHard(), 230 | outfit: { 231 | modifier: "mainstat, 4exp, monster level percent", 232 | equip: $items`makeshift garbage shirt, unbreakable umbrella`, 233 | familiar: $familiar`Left-Hand Man`, 234 | }, 235 | limit: { tries: 5 }, 236 | freecombat: true, 237 | }, 238 | { 239 | name: "Sausage Fights", 240 | after: [], 241 | acquire: [ 242 | { 243 | item: $item`makeshift garbage shirt`, 244 | get: () => cliExecute("fold makeshift garbage shirt"), 245 | }, 246 | ], 247 | ready: () => 248 | have($familiar`Pocket Professor`) && 249 | have($item`Kramco Sausage-o-Matic™`) && 250 | getKramcoWandererChance() === 1, 251 | completed: () => get("_sausageFights") > 0 || myLevel() >= args.levelto || !args.professor, 252 | do: $location`The Outskirts of Cobb's Knob`, 253 | combat: new CombatStrategy() 254 | .macro( 255 | new Macro() 256 | .trySkill($skill`lecture on relativity`) 257 | .trySkill($skill`Saucegeyser`) 258 | .repeat(), 259 | $monster`sausage goblin` 260 | ) 261 | .abort(), // error on everything except sausage goblin 262 | outfit: { 263 | modifier: "mainstat, 4exp", 264 | equip: $items`Kramco Sausage-o-Matic™, makeshift garbage shirt, Pocket Professor memory chip`, 265 | familiar: $familiar`Pocket Professor`, 266 | }, 267 | limit: { tries: 1 }, 268 | freecombat: true, 269 | }, 270 | { 271 | name: "Neverending Party", 272 | after: [], 273 | acquire: [ 274 | { 275 | item: $item`makeshift garbage shirt`, 276 | get: () => cliExecute("fold makeshift garbage shirt"), 277 | }, 278 | ], 279 | completed: () => get("_neverendingPartyFreeTurns") >= 10 || myLevel() >= args.levelto, 280 | do: $location`The Neverending Party`, 281 | choices: { 1322: 2, 1324: 5 }, 282 | combat: new CombatStrategy() 283 | .macro((): Macro => { 284 | if ( 285 | get("_neverendingPartyFreeTurns") >= 7 && 286 | get("_feelPrideUsed") < 3 && 287 | have($skill`Feel Pride`) 288 | ) { 289 | return new Macro().skill($skill`Feel Pride`); 290 | } else if (get("_neverendingPartyFreeTurns") >= 6 && have($item`cosmic bowling ball`)) { 291 | return new Macro().skill($skill`Bowl Sideways`); 292 | } else { 293 | return new Macro(); 294 | } 295 | }) 296 | .killHard(), 297 | outfit: { 298 | modifier: "mainstat, 4exp, monster level percent", 299 | equip: $items`makeshift garbage shirt, unbreakable umbrella`, 300 | familiar: $familiar`Left-Hand Man`, 301 | }, 302 | limit: { tries: 11 }, 303 | freecombat: true, 304 | }, 305 | { 306 | name: "Machine Elf", 307 | after: [], 308 | acquire: [ 309 | { 310 | item: $item`makeshift garbage shirt`, 311 | get: () => cliExecute("fold makeshift garbage shirt"), 312 | }, 313 | ], 314 | ready: () => have($familiar`Machine Elf`), 315 | completed: () => get("_machineTunnelsAdv") >= 5 || myLevel() >= args.levelto, 316 | do: $location`The Deep Machine Tunnels`, 317 | combat: new CombatStrategy().killHard(), 318 | outfit: { 319 | modifier: "mainstat, 4exp, monster level percent", 320 | equip: $items`makeshift garbage shirt, unbreakable umbrella`, 321 | familiar: $familiar`Machine Elf`, 322 | }, 323 | limit: { tries: 5 }, 324 | freecombat: true, 325 | }, 326 | { 327 | name: "Leaflet", 328 | after: [], 329 | ready: () => myLevel() >= 9, 330 | completed: () => get("leafletCompleted"), 331 | do: (): void => { 332 | visitUrl("council.php"); 333 | cliExecute("leaflet"); 334 | set("leafletCompleted", true); 335 | }, 336 | freeaction: true, 337 | limit: { tries: 1 }, 338 | outfit: { 339 | modifier: "exp", 340 | }, 341 | }, 342 | ], 343 | }; 344 | -------------------------------------------------------------------------------- /src/tasks/level11_hidden.ts: -------------------------------------------------------------------------------- 1 | import { cliExecute, myHash, toInt, use, visitUrl } from "kolmafia"; 2 | import { 3 | $effect, 4 | $effects, 5 | $item, 6 | $items, 7 | $location, 8 | $monster, 9 | $monsters, 10 | get, 11 | have, 12 | Macro, 13 | } from "libram"; 14 | import { Quest, Task } from "../engine/task"; 15 | import { CombatStrategy } from "../engine/combat"; 16 | import { runawayValue } from "../engine/resources"; 17 | import { step } from "grimoire-kolmafia"; 18 | 19 | function manualChoice(whichchoice: number, option: number) { 20 | return visitUrl(`choice.php?whichchoice=${whichchoice}&pwd=${myHash()}&option=${option}`); 21 | } 22 | 23 | const Temple: Task[] = [ 24 | { 25 | name: "Forest Coin", 26 | after: ["Mosquito/Burn Delay"], 27 | completed: () => 28 | have($item`tree-holed coin`) || 29 | have($item`Spooky Temple map`) || 30 | step("questM16Temple") === 999, 31 | do: $location`The Spooky Forest`, 32 | choices: { 502: 2, 505: 2, 334: 1 }, 33 | outfit: { modifier: "-combat" }, 34 | limit: { soft: 10 }, 35 | }, 36 | { 37 | name: "Forest Map", 38 | after: ["Forest Coin"], 39 | completed: () => have($item`Spooky Temple map`) || step("questM16Temple") === 999, 40 | do: $location`The Spooky Forest`, 41 | choices: { 502: 3, 506: 3, 507: 1, 334: 1 }, 42 | outfit: { modifier: "-combat" }, 43 | limit: { soft: 10 }, 44 | }, 45 | { 46 | name: "Forest Sapling", 47 | after: ["Mosquito/Burn Delay"], 48 | completed: () => have($item`spooky sapling`) || step("questM16Temple") === 999, 49 | do: $location`The Spooky Forest`, 50 | choices: { 502: 1, 503: 3, 504: 3, 334: 1 }, 51 | outfit: { modifier: "-combat" }, 52 | limit: { soft: 10 }, 53 | }, 54 | { 55 | name: "Open Temple", 56 | after: ["Forest Coin", "Forest Map", "Forest Sapling"], 57 | acquire: [{ item: $item`Spooky-Gro fertilizer` }], 58 | completed: () => step("questM16Temple") === 999, 59 | do: () => use($item`Spooky Temple map`), 60 | limit: { tries: 1 }, 61 | freeaction: true, 62 | }, 63 | { 64 | name: "Temple Nostril", 65 | after: ["Open Temple"], 66 | acquire: [{ item: $item`stone wool` }], 67 | completed: () => have($item`the Nostril of the Serpent`) || step("questL11Worship") >= 3, 68 | do: $location`The Hidden Temple`, 69 | choices: { 579: 2, 582: 1 }, 70 | effects: $effects`Stone-Faced`, 71 | limit: { tries: 1 }, 72 | }, 73 | { 74 | name: "Open City", 75 | after: ["Temple Nostril", "Macguffin/Diary"], 76 | acquire: [{ item: $item`stone wool` }], 77 | completed: () => step("questL11Worship") >= 3, 78 | do: () => { 79 | visitUrl("adventure.php?snarfblat=280"); 80 | manualChoice(582, 2); 81 | manualChoice(580, 2); 82 | manualChoice(584, 4); 83 | manualChoice(580, 1); 84 | manualChoice(123, 2); 85 | visitUrl("choice.php"); 86 | cliExecute("dvorak"); 87 | manualChoice(125, 3); 88 | }, 89 | effects: $effects`Stone-Faced`, 90 | limit: { tries: 1 }, 91 | }, 92 | ]; 93 | 94 | const use_writ = new Macro().if_( 95 | // eslint-disable-next-line libram/verify-constants 96 | `!haseffect ${toInt($effect`Everything Looks Green`)}`, 97 | Macro.tryItem($item`short writ of habeas corpus`) 98 | ); 99 | 100 | const Apartment: Task[] = [ 101 | { 102 | name: "Open Apartment", 103 | after: ["Open City"], 104 | completed: () => get("hiddenApartmentProgress") >= 1, 105 | do: $location`An Overgrown Shrine (Northwest)`, 106 | outfit: { 107 | equip: $items`antique machete`, 108 | }, 109 | choices: { 781: 1 }, 110 | limit: { tries: 4 }, 111 | freecombat: true, 112 | acquire: [{ item: $item`antique machete` }], 113 | }, 114 | { 115 | name: "Apartment Files", // Get the last McClusky files here if needed, as a backup plan 116 | after: ["Office Files"], 117 | completed: () => 118 | have($item`McClusky file (page 5)`) || 119 | have($item`McClusky file (complete)`) || 120 | get("hiddenOfficeProgress") >= 7, 121 | do: $location`The Hidden Apartment Building`, 122 | combat: new CombatStrategy() 123 | .killHard($monster`ancient protector spirit (The Hidden Apartment Building)`) 124 | .kill($monster`pygmy witch accountant`) 125 | .banish($monsters`pygmy janitor, pygmy witch lawyer`) 126 | .macro(new Macro().step(use_writ), $monster`pygmy shaman`) 127 | .ignoreNoBanish($monster`pygmy shaman`) 128 | .ignore(), 129 | limit: { tries: 9 }, 130 | choices: { 780: 1 }, 131 | }, 132 | { 133 | name: "Apartment", 134 | after: ["Open Apartment", "Apartment Files"], // Wait until after all needed pygmy witch lawyers are done 135 | completed: () => get("hiddenApartmentProgress") >= 7, 136 | acquire: [ 137 | { item: $item`short writ of habeas corpus`, num: 1, price: runawayValue, optional: true }, 138 | ], 139 | do: $location`The Hidden Apartment Building`, 140 | combat: new CombatStrategy() 141 | .killHard($monster`ancient protector spirit (The Hidden Apartment Building)`) 142 | .banish($monsters`pygmy janitor, pygmy witch lawyer, pygmy witch accountant`) 143 | .macro(new Macro().step(use_writ), $monster`pygmy shaman`) 144 | .ignoreNoBanish($monster`pygmy shaman`) 145 | .ignore(), 146 | choices: { 780: 1 }, 147 | limit: { tries: 9 }, 148 | }, 149 | { 150 | name: "Finish Apartment", 151 | after: ["Apartment"], 152 | completed: () => get("hiddenApartmentProgress") >= 8, 153 | do: $location`An Overgrown Shrine (Northwest)`, 154 | choices: { 781: 2 }, 155 | limit: { tries: 1 }, 156 | freeaction: true, 157 | }, 158 | ]; 159 | 160 | const Office: Task[] = [ 161 | { 162 | name: "Open Office", 163 | after: ["Open City"], 164 | completed: () => get("hiddenOfficeProgress") >= 1, 165 | do: $location`An Overgrown Shrine (Northeast)`, 166 | outfit: { 167 | equip: $items`antique machete`, 168 | }, 169 | choices: { 785: 1 }, 170 | limit: { tries: 4 }, 171 | freecombat: true, 172 | acquire: [{ item: $item`antique machete` }], 173 | }, 174 | { 175 | name: "Office Files", 176 | after: ["Open Office"], 177 | completed: () => 178 | (have($item`McClusky file (page 1)`) && 179 | have($item`McClusky file (page 2)`) && 180 | have($item`McClusky file (page 3)`) && 181 | have($item`McClusky file (page 4)`) && 182 | have($item`McClusky file (page 5)`)) || 183 | have($item`McClusky file (complete)`) || 184 | get("hiddenOfficeProgress") >= 7 || 185 | $location`The Hidden Office Building`.turnsSpent >= 10, 186 | do: $location`The Hidden Office Building`, 187 | combat: new CombatStrategy() 188 | .kill($monster`pygmy witch accountant`) 189 | .banish($monsters`pygmy janitor, pygmy headhunter, pygmy witch lawyer`), 190 | choices: { 786: 2 }, 191 | limit: { tries: 10 }, 192 | }, 193 | { 194 | name: "Office Clip", 195 | after: ["Office Files", "Apartment Files"], 196 | acquire: [ 197 | { item: $item`short writ of habeas corpus`, num: 1, price: runawayValue, optional: true }, 198 | ], 199 | completed: () => 200 | have($item`boring binder clip`) || 201 | have($item`McClusky file (complete)`) || 202 | get("hiddenOfficeProgress") >= 7, 203 | do: $location`The Hidden Office Building`, 204 | choices: { 786: 2 }, 205 | combat: new CombatStrategy() 206 | .macro( 207 | use_writ, 208 | $monsters`pygmy witch accountant, pygmy janitor, pygmy headhunter, pygmy witch lawyer` 209 | ) 210 | .ignore(), 211 | limit: { tries: 6 }, 212 | }, 213 | { 214 | name: "Office Boss", 215 | after: ["Office Clip"], 216 | acquire: [ 217 | { item: $item`short writ of habeas corpus`, num: 1, price: runawayValue, optional: true }, 218 | ], 219 | completed: () => get("hiddenOfficeProgress") >= 7, 220 | do: $location`The Hidden Office Building`, 221 | choices: { 786: 1 }, 222 | combat: new CombatStrategy() 223 | .killHard($monster`ancient protector spirit (The Hidden Office Building)`) 224 | .macro( 225 | use_writ, 226 | $monsters`pygmy witch accountant, pygmy janitor, pygmy headhunter, pygmy witch lawyer` 227 | ) 228 | .ignore(), 229 | limit: { tries: 5 }, 230 | }, 231 | { 232 | name: "Finish Office", 233 | after: ["Office Boss"], 234 | completed: () => get("hiddenOfficeProgress") >= 8, 235 | do: $location`An Overgrown Shrine (Northeast)`, 236 | choices: { 785: 2 }, 237 | limit: { tries: 1 }, 238 | freeaction: true, 239 | }, 240 | ]; 241 | 242 | const Hospital: Task[] = [ 243 | { 244 | name: "Open Hospital", 245 | after: ["Open City"], 246 | completed: () => get("hiddenHospitalProgress") >= 1, 247 | do: $location`An Overgrown Shrine (Southwest)`, 248 | outfit: { 249 | equip: $items`antique machete`, 250 | }, 251 | choices: { 783: 1 }, 252 | limit: { tries: 4 }, 253 | freecombat: true, 254 | acquire: [{ item: $item`antique machete` }], 255 | }, 256 | { 257 | name: "Hospital", 258 | after: ["Open Hospital"], 259 | acquire: [ 260 | { item: $item`short writ of habeas corpus`, num: 1, price: runawayValue, optional: true }, 261 | { item: $item`half-size scalpel` }, 262 | { item: $item`head mirror` }, 263 | { item: $item`surgical mask` }, 264 | { item: $item`surgical apron` }, 265 | { item: $item`bloodied surgical dungarees` }, 266 | ], 267 | completed: () => get("hiddenHospitalProgress") >= 7, 268 | do: $location`The Hidden Hospital`, 269 | combat: new CombatStrategy() 270 | .killHard($monster`ancient protector spirit (The Hidden Hospital)`) 271 | .macro( 272 | use_writ, 273 | $monsters`pygmy orderlies, pygmy janitor, pygmy witch nurse, pygmy witch surgeon` 274 | ) 275 | .ignore(), 276 | outfit: { 277 | equip: $items`half-size scalpel, head mirror, surgical mask, surgical apron, bloodied surgical dungarees`, 278 | }, 279 | choices: { 784: 1 }, 280 | limit: { soft: 10 }, 281 | }, 282 | { 283 | name: "Finish Hospital", 284 | after: ["Hospital"], 285 | completed: () => get("hiddenHospitalProgress") >= 8, 286 | do: $location`An Overgrown Shrine (Southwest)`, 287 | choices: { 783: 2 }, 288 | limit: { tries: 1 }, 289 | freeaction: true, 290 | }, 291 | ]; 292 | 293 | const Bowling: Task[] = [ 294 | { 295 | name: "Open Bowling", 296 | after: ["Open City"], 297 | completed: () => get("hiddenBowlingAlleyProgress") >= 1, 298 | do: $location`An Overgrown Shrine (Southeast)`, 299 | outfit: { 300 | equip: $items`antique machete`, 301 | }, 302 | choices: { 787: 1 }, 303 | limit: { tries: 4 }, 304 | freecombat: true, 305 | acquire: [{ item: $item`antique machete` }], 306 | }, 307 | { 308 | name: "Bowling", 309 | after: ["Open Bowling"], 310 | acquire: [{ item: $item`bowling ball` }], 311 | completed: () => get("hiddenBowlingAlleyProgress") >= 7, 312 | do: $location`The Hidden Bowling Alley`, 313 | combat: new CombatStrategy().killHard( 314 | $monster`ancient protector spirit (The Hidden Bowling Alley)` 315 | ), 316 | choices: { 788: 1 }, 317 | limit: { tries: 5 }, 318 | }, 319 | { 320 | name: "Finish Bowling", 321 | after: ["Bowling"], 322 | completed: () => get("hiddenBowlingAlleyProgress") >= 8, 323 | do: $location`An Overgrown Shrine (Southeast)`, 324 | choices: { 787: 2 }, 325 | limit: { tries: 1 }, 326 | freeaction: true, 327 | }, 328 | ]; 329 | 330 | export const HiddenQuest: Quest = { 331 | name: "Hidden City", 332 | tasks: [ 333 | ...Temple, 334 | ...Office, 335 | ...Apartment, 336 | ...Hospital, 337 | ...Bowling, 338 | { 339 | name: "Boss", 340 | after: ["Finish Office", "Finish Apartment", "Finish Hospital", "Finish Bowling"], 341 | completed: () => step("questL11Worship") === 999, 342 | do: $location`A Massive Ziggurat`, 343 | outfit: { 344 | equip: $items`antique machete`, 345 | }, 346 | choices: { 791: 1 }, 347 | boss: true, 348 | combat: new CombatStrategy().kill($monsters`dense liana, Protector Spectre`), 349 | limit: { tries: 4 }, 350 | acquire: [{ item: $item`antique machete` }], 351 | }, 352 | ], 353 | }; 354 | -------------------------------------------------------------------------------- /src/tasks/level13.ts: -------------------------------------------------------------------------------- 1 | import { myAdventures, myLevel, runChoice, useSkill, visitUrl } from "kolmafia"; 2 | import { $effects, $familiar, $item, $items, $location, $skill, $stat, get, Macro } from "libram"; 3 | import { CombatStrategy } from "../engine/combat"; 4 | import { Quest, Task } from "../engine/task"; 5 | import { step } from "grimoire-kolmafia"; 6 | 7 | const Challenges: Task[] = [ 8 | { 9 | name: "Speed Challenge", 10 | after: ["Start"], 11 | completed: () => get("nsContestants1") > -1, 12 | do: (): void => { 13 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 14 | runChoice(1); 15 | runChoice(6); 16 | }, 17 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "init" }, 18 | limit: { tries: 1 }, 19 | freeaction: true, 20 | }, 21 | { 22 | name: "Moxie Challenge", 23 | after: ["Start"], 24 | ready: () => get("nsChallenge1") === $stat`Moxie`, 25 | completed: () => get("nsContestants2") > -1, 26 | do: (): void => { 27 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 28 | runChoice(2); 29 | runChoice(6); 30 | }, 31 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "moxie" }, 32 | limit: { tries: 1 }, 33 | freeaction: true, 34 | }, 35 | { 36 | name: "Muscle Challenge", 37 | after: ["Start"], 38 | ready: () => get("nsChallenge1") === $stat`Muscle`, 39 | completed: () => get("nsContestants2") > -1, 40 | do: (): void => { 41 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 42 | runChoice(2); 43 | runChoice(6); 44 | }, 45 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "muscle" }, 46 | limit: { tries: 1 }, 47 | freeaction: true, 48 | }, 49 | { 50 | name: "Mysticality Challenge", 51 | after: ["Start"], 52 | ready: () => get("nsChallenge1") === $stat`Mysticality`, 53 | completed: () => get("nsContestants2") > -1, 54 | do: (): void => { 55 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 56 | runChoice(2); 57 | runChoice(6); 58 | }, 59 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "mysticality" }, 60 | limit: { tries: 1 }, 61 | freeaction: true, 62 | }, 63 | { 64 | name: "Hot Challenge", 65 | after: ["Start"], 66 | ready: () => get("nsChallenge2") === "hot", 67 | completed: () => get("nsContestants3") > -1, 68 | do: (): void => { 69 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 70 | runChoice(3); 71 | runChoice(6); 72 | }, 73 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "hot dmg, hot spell dmg" }, 74 | limit: { tries: 1 }, 75 | freeaction: true, 76 | }, 77 | { 78 | name: "Cold Challenge", 79 | after: ["Start"], 80 | ready: () => get("nsChallenge2") === "cold", 81 | completed: () => get("nsContestants3") > -1, 82 | do: (): void => { 83 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 84 | runChoice(3); 85 | runChoice(6); 86 | }, 87 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "cold dmg, cold spell dmg" }, 88 | limit: { tries: 1 }, 89 | freeaction: true, 90 | }, 91 | { 92 | name: "Spooky Challenge", 93 | after: ["Start"], 94 | ready: () => get("nsChallenge2") === "spooky", 95 | completed: () => get("nsContestants3") > -1, 96 | do: (): void => { 97 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 98 | runChoice(3); 99 | runChoice(6); 100 | }, 101 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "spooky dmg, spooky spell dmg" }, 102 | limit: { tries: 1 }, 103 | freeaction: true, 104 | }, 105 | { 106 | name: "Stench Challenge", 107 | after: ["Start"], 108 | ready: () => get("nsChallenge2") === "stench", 109 | completed: () => get("nsContestants3") > -1, 110 | do: (): void => { 111 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 112 | runChoice(3); 113 | runChoice(6); 114 | }, 115 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "stench dmg, stench spell dmg" }, 116 | limit: { tries: 1 }, 117 | freeaction: true, 118 | }, 119 | { 120 | name: "Sleaze Challenge", 121 | after: ["Start"], 122 | ready: () => get("nsChallenge2") === "sleaze", 123 | completed: () => get("nsContestants3") > -1, 124 | do: (): void => { 125 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 126 | runChoice(3); 127 | runChoice(6); 128 | }, 129 | outfit: { familiar: $familiar`Left-Hand Man`, modifier: "sleaze dmg, sleaze spell dmg" }, 130 | limit: { tries: 1 }, 131 | freeaction: true, 132 | }, 133 | ]; 134 | 135 | const ChallengeBosses: Task[] = [ 136 | { 137 | name: "Speed Boss", 138 | after: ["Speed Challenge"], 139 | completed: () => get("nsContestants1") === 0, 140 | do: $location`Fastest Adventurer Contest`, 141 | boss: true, 142 | combat: new CombatStrategy().killHard(), 143 | limit: { tries: 1 }, 144 | }, 145 | { 146 | name: "Stat Boss", 147 | after: ["Muscle Challenge", "Moxie Challenge", "Mysticality Challenge"], 148 | completed: () => get("nsContestants2") === 0, 149 | do: $location`A Crowd of (Stat) Adventurers`, 150 | boss: true, 151 | combat: new CombatStrategy().killHard(), 152 | limit: { tries: 1 }, 153 | }, 154 | { 155 | name: "Element Boss", 156 | after: [ 157 | "Hot Challenge", 158 | "Cold Challenge", 159 | "Spooky Challenge", 160 | "Stench Challenge", 161 | "Sleaze Challenge", 162 | ], 163 | completed: () => get("nsContestants3") === 0, 164 | do: $location`A Crowd of (Element) Adventurers`, 165 | boss: true, 166 | combat: new CombatStrategy().killHard(), 167 | limit: { tries: 1 }, 168 | }, 169 | ]; 170 | 171 | const Door: Task[] = [ 172 | { 173 | name: "Boris Lock", 174 | after: ["Maze", "Keys/Finish"], 175 | acquire: [{ item: $item`Boris's key` }], 176 | completed: () => get("nsTowerDoorKeysUsed").includes("Boris"), 177 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_lock1"), 178 | limit: { tries: 1 }, 179 | freeaction: true, 180 | }, 181 | { 182 | name: "Jarlsberg Lock", 183 | after: ["Maze", "Keys/Finish"], 184 | acquire: [{ item: $item`Jarlsberg's key` }], 185 | completed: () => get("nsTowerDoorKeysUsed").includes("Jarlsberg"), 186 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_lock2"), 187 | limit: { tries: 1 }, 188 | freeaction: true, 189 | }, 190 | { 191 | name: "Sneaky Pete Lock", 192 | after: ["Maze", "Keys/Finish"], 193 | acquire: [{ item: $item`Sneaky Pete's key` }], 194 | completed: () => get("nsTowerDoorKeysUsed").includes("Sneaky Pete"), 195 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_lock3"), 196 | limit: { tries: 1 }, 197 | freeaction: true, 198 | }, 199 | { 200 | name: "Star Lock", 201 | after: ["Maze"], 202 | acquire: [{ item: $item`Richard's star key` }], 203 | completed: () => get("nsTowerDoorKeysUsed").includes("Richard's star key"), 204 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_lock4"), 205 | limit: { tries: 1 }, 206 | freeaction: true, 207 | }, 208 | { 209 | name: "Digital Lock", 210 | after: ["Maze", "Digital/Key"], 211 | completed: () => get("nsTowerDoorKeysUsed").includes("digital key"), 212 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_lock5"), 213 | limit: { tries: 1 }, 214 | freeaction: true, 215 | }, 216 | { 217 | name: "Skeleton Lock", 218 | after: ["Maze"], 219 | acquire: [{ item: $item`skeleton key` }], 220 | completed: () => get("nsTowerDoorKeysUsed").includes("skeleton key"), 221 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_lock6"), 222 | limit: { tries: 1 }, 223 | freeaction: true, 224 | }, 225 | { 226 | name: "Door", 227 | after: [ 228 | "Boris Lock", 229 | "Jarlsberg Lock", 230 | "Sneaky Pete Lock", 231 | "Star Lock", 232 | "Digital Lock", 233 | "Skeleton Lock", 234 | ], 235 | completed: () => step("questL13Final") > 5, 236 | do: () => visitUrl("place.php?whichplace=nstower_door&action=ns_doorknob"), 237 | limit: { tries: 1 }, 238 | freeaction: true, 239 | }, 240 | ]; 241 | 242 | export const TowerQuest: Quest = { 243 | name: "Tower", 244 | tasks: [ 245 | { 246 | name: "Start", 247 | after: [ 248 | "Mosquito/Finish", 249 | "Tavern/Finish", 250 | "Bat/Finish", 251 | "Knob/King", 252 | "Friar/Finish", 253 | "Crypt/Finish", 254 | "McLargeHuge/Finish", 255 | "Orc Chasm/Finish", 256 | "Giant/Finish", 257 | "Macguffin/Finish", 258 | "War/Boss Hippie", 259 | "War/Boss Frat", 260 | ], 261 | ready: () => myLevel() >= 13, 262 | completed: () => step("questL13Final") !== -1, 263 | do: () => visitUrl("council.php"), 264 | limit: { tries: 1 }, 265 | freeaction: true, 266 | }, 267 | ...Challenges, 268 | ...ChallengeBosses, 269 | { 270 | name: "Coronation", 271 | after: ["Speed Boss", "Stat Boss", "Element Boss"], 272 | completed: () => step("questL13Final") > 2, 273 | do: (): void => { 274 | visitUrl("place.php?whichplace=nstower&action=ns_01_contestbooth"); 275 | runChoice(-1); 276 | }, 277 | choices: { 1003: 4 }, 278 | limit: { tries: 1 }, 279 | }, 280 | { 281 | name: "Frank", 282 | after: ["Coronation"], 283 | completed: () => step("questL13Final") > 3, 284 | do: (): void => { 285 | visitUrl("place.php?whichplace=nstower&action=ns_02_coronation"); 286 | runChoice(-1); 287 | }, 288 | choices: { 1020: 1, 1021: 1, 1022: 1 }, 289 | limit: { tries: 1 }, 290 | }, 291 | { 292 | name: "Maze", 293 | after: ["Frank"], 294 | ready: () => myAdventures() >= 4, 295 | completed: () => step("questL13Final") > 4, 296 | prepare: () => useSkill($skill`Cannelloni Cocoon`), 297 | do: $location`The Hedge Maze`, 298 | choices: { 1004: 1, 1005: 2, 1008: 2, 1011: 2, 1013: 1, 1022: 1 }, 299 | outfit: { 300 | modifier: "hot res, cold res, stench res, spooky res, sleaze res", 301 | familiar: $familiar`Exotic Parrot`, 302 | }, 303 | limit: { tries: 1 }, 304 | }, 305 | ...Door, 306 | { 307 | name: "Wall of Skin", 308 | after: ["Door"], 309 | prepare: () => useSkill($skill`Cannelloni Cocoon`), 310 | completed: () => step("questL13Final") > 6, 311 | do: $location`Tower Level 1`, 312 | effects: $effects`Spiky Shell, Jalapeño Saucesphere, Psalm of Pointiness, Scarysauce`, 313 | outfit: { familiar: $familiar`Shorter-Order Cook`, equip: $items`bejeweled cufflinks` }, 314 | boss: true, 315 | combat: new CombatStrategy().macro(new Macro().attack().repeat()), 316 | limit: { tries: 1 }, 317 | }, 318 | { 319 | name: "Wall of Meat", 320 | after: ["Wall of Skin"], 321 | prepare: () => useSkill($skill`Cannelloni Cocoon`), 322 | completed: () => step("questL13Final") > 7, 323 | do: $location`Tower Level 2`, 324 | outfit: { modifier: "meat", skipDefaults: true, familiar: $familiar`Hobo Monkey` }, 325 | boss: true, 326 | combat: new CombatStrategy().killHard(), 327 | limit: { tries: 1 }, 328 | }, 329 | { 330 | name: "Wall of Bones", 331 | after: ["Wall of Meat"], 332 | prepare: () => useSkill($skill`Cannelloni Cocoon`), 333 | completed: () => step("questL13Final") > 8, 334 | do: $location`Tower Level 3`, 335 | outfit: { modifier: "spell dmg" }, 336 | boss: true, 337 | combat: new CombatStrategy().macro(new Macro().skill($skill`Garbage Nova`).repeat()), 338 | limit: { tries: 1 }, 339 | }, 340 | { 341 | name: "Mirror", 342 | after: ["Wall of Bones"], 343 | acquire: [{ item: $item`Wand of Nagamar` }], 344 | completed: () => step("questL13Final") > 9, 345 | do: $location`Tower Level 4`, 346 | choices: { 1015: 2 }, 347 | limit: { tries: 1 }, 348 | freeaction: true, 349 | }, 350 | { 351 | name: "Shadow", 352 | after: ["Mirror"], 353 | acquire: [{ item: $item`gauze garter`, num: 6 }], 354 | prepare: () => useSkill($skill`Cannelloni Cocoon`), 355 | completed: () => step("questL13Final") > 10, 356 | do: $location`Tower Level 5`, 357 | outfit: { modifier: "init" }, 358 | boss: true, 359 | combat: new CombatStrategy().macro( 360 | new Macro().item([$item`gauze garter`, $item`gauze garter`]).repeat() 361 | ), 362 | limit: { tries: 1 }, 363 | }, 364 | { 365 | name: "Naughty Sorceress", 366 | after: ["Shadow"], 367 | prepare: () => useSkill($skill`Cannelloni Cocoon`), 368 | completed: () => step("questL13Final") > 11, 369 | do: $location`The Naughty Sorceress' Chamber`, 370 | outfit: { modifier: "muscle" }, 371 | boss: true, 372 | combat: new CombatStrategy().kill(), 373 | limit: { tries: 1 }, 374 | }, 375 | { 376 | name: "Finish", 377 | after: ["Naughty Sorceress"], 378 | completed: () => step("questL13Final") === 999, 379 | do: () => visitUrl("place.php?whichplace=nstower&action=ns_11_prism"), 380 | limit: { tries: 1 }, 381 | freeaction: true, 382 | noadventures: true, 383 | }, 384 | ], 385 | }; 386 | -------------------------------------------------------------------------------- /src/tasks/diet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | availableAmount, 3 | buy, 4 | chew, 5 | cliExecute, 6 | drink, 7 | eat, 8 | equip, 9 | familiarEquippedEquipment, 10 | getIngredients, 11 | haveEffect, 12 | Item, 13 | itemAmount, 14 | itemType, 15 | mallPrice, 16 | myAdventures, 17 | myBasestat, 18 | myDaycount, 19 | myFullness, 20 | myInebriety, 21 | myPrimestat, 22 | mySpleenUse, 23 | print, 24 | restoreMp, 25 | retrieveItem, 26 | reverseNumberology, 27 | setProperty, 28 | toInt, 29 | turnsPerCast, 30 | use, 31 | useFamiliar, 32 | useSkill, 33 | } from "kolmafia"; 34 | import { 35 | $effect, 36 | $familiar, 37 | $item, 38 | $items, 39 | $skill, 40 | $slot, 41 | clamp, 42 | Diet, 43 | get, 44 | getAverageAdventures, 45 | getRemainingLiver, 46 | getRemainingSpleen, 47 | getRemainingStomach, 48 | have, 49 | MenuItem, 50 | sumNumbers, 51 | } from "libram"; 52 | import { args } from "../main"; 53 | import { Quest } from "../engine/task"; 54 | 55 | export const DietQuest: Quest = { 56 | name: "Diet", 57 | tasks: [ 58 | { 59 | name: "Consume", 60 | after: [], 61 | completed: () => 62 | myDaycount() > 1 || (myFullness() >= args.stomach && myInebriety() >= args.liver), 63 | ready: () => myBasestat(myPrimestat()) >= 149 || myAdventures() <= 1, 64 | do: (): void => { 65 | if (have($item`astral six-pack`)) { 66 | use($item`astral six-pack`); 67 | } 68 | const MPA = args.voa; 69 | 70 | // Use the mime shotglass if available 71 | if (!get("_mimeArmyShotglassUsed") && have($item`mime army shotglass`)) { 72 | const shotglassDiet = Diet.plan(MPA, shotglassMenu(), { food: 0, booze: 1, spleen: 0 }); 73 | consumeDiet(shotglassDiet, MPA); 74 | } 75 | 76 | // Compute a diet to bring us up to the desired usage 77 | const food = Math.max(args.stomach - myFullness(), 0); 78 | const booze = Math.max(args.liver - myInebriety(), 0); 79 | const spleen = Math.max(args.spleen - mySpleenUse(), 0); 80 | const plannedDiet = Diet.plan(MPA, menu(), { 81 | food: food, 82 | booze: booze, 83 | spleen: spleen, 84 | }); 85 | 86 | // Eat the diet 87 | consumeDiet(plannedDiet, MPA); 88 | }, 89 | limit: { tries: 1 }, 90 | freeaction: true, 91 | noadventures: true, 92 | }, 93 | { 94 | name: "Numberology", 95 | after: [], 96 | completed: () => get("_universeCalculated") >= get("skillLevel144"), 97 | ready: () => myAdventures() > 0 && Object.keys(reverseNumberology()).includes("69"), 98 | do: (): void => { 99 | restoreMp(1); 100 | cliExecute("numberology 69"); 101 | }, 102 | limit: { tries: 5 }, 103 | freeaction: true, 104 | noadventures: true, 105 | }, 106 | { 107 | name: "Sausage", 108 | after: ["Consume"], 109 | completed: () => !have($item`Kramco Sausage-o-Matic™`) || get("_sausagesEaten") >= 23, // Cap at 23 sausages to avoid burning through an entire supply 110 | ready: () => have($item`magical sausage casing`), 111 | do: (): void => { 112 | // Pump-and-grind cannot be used from Left-Hand Man 113 | if ( 114 | have($familiar`Left-Hand Man`) && 115 | familiarEquippedEquipment($familiar`Left-Hand Man`) === $item`Kramco Sausage-o-Matic™` 116 | ) { 117 | useFamiliar($familiar`Left-Hand Man`); 118 | equip($slot`familiar`, $item`none`); 119 | } 120 | eat(1, $item`magical sausage`); 121 | }, 122 | limit: { tries: 23 }, 123 | freeaction: true, 124 | noadventures: true, 125 | }, 126 | { 127 | name: "Hourglass", 128 | after: [], 129 | completed: () => !have($item`etched hourglass`) || get("_etchedHourglassUsed"), 130 | do: (): void => { 131 | use($item`etched hourglass`); 132 | }, 133 | limit: { tries: 1 }, 134 | freeaction: true, 135 | noadventures: true, 136 | }, 137 | ], 138 | }; 139 | 140 | const spleenCleaners = new Map([ 141 | [$item`extra-greasy slider`, 5], 142 | [$item`jar of fermented pickle juice`, 5], 143 | [$item`mojo filter`, 1], 144 | ]); 145 | 146 | function priceToCraft(item: Item) { 147 | if (item.tradeable) { 148 | return mallPrice(item); 149 | } 150 | let total = 0; 151 | const ingredients = getIngredients(item); 152 | for (const i in ingredients) { 153 | total += priceToCraft($item`${i}`) * ingredients[i]; 154 | } 155 | return total; 156 | } 157 | 158 | function acquire(qty: number, item: Item, maxPrice?: number, throwOnFail = true): number { 159 | const startAmount = itemAmount(item); 160 | const remaining = qty - startAmount; 161 | if (maxPrice === undefined) throw `No price cap for ${item.name}.`; 162 | if ( 163 | $items`Boris's bread, roasted vegetable of Jarlsberg, Pete's rich ricotta, roasted vegetable focaccia, baked veggie ricotta casserole, plain calzone, Deep Dish of Legend, Calzone of Legend, Pizza of Legend`.includes( 164 | item 165 | ) 166 | ) { 167 | print(`Trying to acquire ${qty} ${item.plural}; max price ${maxPrice.toFixed(0)}.`, "green"); 168 | if (priceToCraft(item) <= maxPrice) { 169 | retrieveItem(remaining, item); 170 | } 171 | return itemAmount(item) - startAmount; 172 | } 173 | if (!item.tradeable || (maxPrice !== undefined && maxPrice <= 0)) return 0; 174 | 175 | print(`Trying to acquire ${qty} ${item.plural}; max price ${maxPrice.toFixed(0)}.`, "green"); 176 | 177 | if (qty * mallPrice(item) > 1000000) throw "Aggregate cost too high! Probably a bug."; 178 | 179 | if (remaining <= 0) return qty; 180 | if (maxPrice <= 0) throw `buying disabled for ${item.name}.`; 181 | 182 | buy(remaining, item, maxPrice); 183 | if (itemAmount(item) < qty && throwOnFail) throw `Mall price too high for ${item.name}.`; 184 | return itemAmount(item) - startAmount; 185 | } 186 | 187 | function argmax(values: [T, number][]): T { 188 | return values.reduce(([minValue, minScore], [value, score]) => 189 | score > minScore ? [value, score] : [minValue, minScore] 190 | )[0]; 191 | } 192 | 193 | function eatSafe(qty: number, item: Item, mpa: number) { 194 | if (!get("_milkOfMagnesiumUsed")) { 195 | acquire(1, $item`milk of magnesium`, 5 * mpa); 196 | use($item`milk of magnesium`); 197 | } 198 | if (!eat(qty, item)) throw "Failed to eat safely"; 199 | } 200 | 201 | function drinkSafe(qty: number, item: Item) { 202 | const prevDrunk = myInebriety(); 203 | if (have($skill`The Ode to Booze`)) { 204 | const odeTurns = qty * item.inebriety; 205 | const castTurns = odeTurns - haveEffect($effect`Ode to Booze`); 206 | if (castTurns > 0) { 207 | useSkill( 208 | $skill`The Ode to Booze`, 209 | Math.ceil(castTurns / turnsPerCast($skill`The Ode to Booze`)) 210 | ); 211 | } 212 | } 213 | if (!drink(qty, item)) throw "Failed to drink safely"; 214 | if (item.inebriety === 1 && prevDrunk === qty + myInebriety() - 1) { 215 | // sometimes mafia does not track the mime army shotglass property 216 | setProperty("_mimeArmyShotglassUsed", "true"); 217 | } 218 | } 219 | 220 | function chewSafe(qty: number, item: Item) { 221 | if (!chew(qty, item)) throw "Failed to chew safely"; 222 | } 223 | 224 | type MenuData = { 225 | turns: number; // Est. number of turns provided by an item; used for the price cap 226 | }; 227 | function consumeSafe( 228 | qty: number, 229 | item: Item, 230 | mpa: number, 231 | data?: MenuData, 232 | additionalValue?: number, 233 | skipAcquire?: boolean 234 | ) { 235 | const spleenCleaned = spleenCleaners.get(item); 236 | if (spleenCleaned && mySpleenUse() < spleenCleaned) { 237 | throw "No spleen to clear with this."; 238 | } 239 | const averageAdventures = data?.turns ?? getAverageAdventures(item); 240 | if (!skipAcquire && (averageAdventures > 0 || additionalValue)) { 241 | const cap = Math.max(0, averageAdventures * mpa) + (additionalValue ?? 0); 242 | acquire(qty, item, cap); 243 | } else if (!skipAcquire) { 244 | acquire(qty, item); 245 | } 246 | if (itemType(item) === "food") eatSafe(qty, item, mpa); 247 | else if (itemType(item) === "booze") drinkSafe(qty, item); 248 | else if (itemType(item) === "spleen item") chewSafe(qty, item); 249 | else if (item !== $item`Special Seasoning`) use(qty, item); 250 | } 251 | 252 | // Item priority - higher means we eat it first. 253 | // Anything that gives a consumption buff should go first (e.g. Refined Palate). 254 | function itemPriority(menuItems: MenuItem[]) { 255 | // Last menu item is the food itself. 256 | const menuItem = menuItems[menuItems.length - 1]; 257 | if (menuItem === undefined) { 258 | throw "Shouldn't have an empty menu item."; 259 | } 260 | if (menuItem.item === $item`spaghetti breakfast`) return 200; 261 | if ( 262 | $items`pocket wish, toasted brie`.includes(menuItem.item) || 263 | spleenCleaners.get(menuItem.item) 264 | ) { 265 | return 100; 266 | } else { 267 | return 0; 268 | } 269 | } 270 | 271 | function recipeKnown(item: Item) { 272 | if ($items`Boris's bread, roasted vegetable of Jarlsberg, Pete's rich ricotta`.includes(item)) { 273 | return !get(`unknownRecipe${toInt(item)}`); 274 | } 275 | let allComponentsKnown = !get(`unknownRecipe${toInt(item)}`); 276 | const ingredients = getIngredients(item); 277 | for (const i in ingredients) { 278 | allComponentsKnown = allComponentsKnown && recipeKnown($item`${i}`); 279 | } 280 | return allComponentsKnown; 281 | } 282 | 283 | function cookBookBatMenu(): MenuItem[] { 284 | /* Excluding 285 | - plain calzone, because the +ML buff may not be desirable 286 | - Deep Dish of Legend, because the +familiar weight buff is best saved for garbo 287 | */ 288 | const cookBookBatFoods = $items`Boris's bread, roasted vegetable of Jarlsberg, Pete's rich ricotta, roasted vegetable focaccia, baked veggie ricotta casserole, Calzone of Legend, Pizza of Legend`; 289 | 290 | const legendaryPizzasEaten: Item[] = []; 291 | if (get("calzoneOfLegendEaten")) legendaryPizzasEaten.push($item`Calzone of Legend`); 292 | if (get("pizzaOfLegendEaten")) legendaryPizzasEaten.push($item`Pizza of Legend`); 293 | if (get("deepDishOfLegendEaten")) legendaryPizzasEaten.push($item`Deep Dish of Legend`); 294 | 295 | const cookBookBatFoodAvailable = cookBookBatFoods.filter( 296 | (food) => recipeKnown(food) && !legendaryPizzasEaten.includes(food) 297 | ); 298 | return cookBookBatFoodAvailable.map( 299 | (food) => 300 | new MenuItem(food, { 301 | priceOverride: priceToCraft(food), 302 | maximum: $items`Calzone of Legend, Pizza of Legend, Deep Dish of Legend`.includes(food) 303 | ? 1 304 | : 99, 305 | }) 306 | ); 307 | } 308 | 309 | function menu(): MenuItem[] { 310 | const spaghettiBreakfast = 311 | have($item`spaghetti breakfast`) && 312 | myFullness() === 0 && 313 | get("_timeSpinnerFoodAvailable") === "" && 314 | !get("_spaghettiBreakfastEaten") 315 | ? 1 316 | : 0; 317 | 318 | const complexMushroomWines = $items`overpowering mushroom wine, complex mushroom wine, smooth mushroom wine, blood-red mushroom wine, buzzing mushroom wine, swirling mushroom wine`; 319 | const perfectDrinks = $items`perfect cosmopolitan, perfect negroni, perfect dark and stormy, perfect mimosa, perfect old-fashioned, perfect paloma`; 320 | const lasagnas = $items`fishy fish lasagna, gnat lasagna, long pork lasagna`; 321 | const smallEpics = $items`meteoreo, ice rice`.concat([$item`Tea, Earl Grey, Hot`]); 322 | 323 | const mallMin = (items: Item[]) => argmax(items.map((i) => [i, -mallPrice(i)])); 324 | 325 | const menu: MenuItem[] = [ 326 | // FOOD 327 | new MenuItem($item`Dreadsylvanian spooky pocket`), 328 | new MenuItem($item`tin cup of mulligan stew`), 329 | new MenuItem($item`frozen banquet`), 330 | new MenuItem($item`deviled egg`), 331 | new MenuItem($item`spaghetti breakfast`, { maximum: spaghettiBreakfast }), 332 | new MenuItem($item`extra-greasy slider`), 333 | new MenuItem(mallMin(lasagnas)), 334 | new MenuItem(mallMin(smallEpics)), 335 | 336 | // BOOZE 337 | new MenuItem($item`astral pilsner`, { maximum: availableAmount($item`astral pilsner`) }), 338 | new MenuItem($item`elemental caipiroska`), 339 | new MenuItem($item`moreltini`), 340 | new MenuItem($item`Dreadsylvanian grimlet`), 341 | new MenuItem($item`Hodgman's blanket`), 342 | new MenuItem($item`Sacramento wine`), 343 | new MenuItem($item`iced plum wine`), 344 | new MenuItem($item`splendid martini`), 345 | new MenuItem($item`Eye and a Twist`), 346 | new MenuItem($item`jar of fermented pickle juice`), 347 | new MenuItem(mallMin(complexMushroomWines)), 348 | new MenuItem(mallMin(perfectDrinks)), 349 | 350 | // SPLEEN 351 | new MenuItem($item`octolus oculus`), 352 | new MenuItem($item`prismatic wad`), 353 | new MenuItem($item`transdermal smoke patch`), 354 | new MenuItem($item`antimatter wad`), 355 | new MenuItem($item`voodoo snuff`), 356 | new MenuItem($item`blood-drive sticker`), 357 | 358 | // HELPERS 359 | new MenuItem($item`Special Seasoning`, { data: { turns: 1 } }), 360 | new MenuItem($item`pocket wish`, { 361 | maximum: 1, 362 | effect: $effect`Refined Palate`, 363 | data: { turns: 10 }, 364 | }), 365 | new MenuItem($item`toasted brie`, { maximum: 1, data: { turns: 10 } }), 366 | new MenuItem($item`potion of the field gar`, { maximum: 1, data: { turns: 5 } }), 367 | ]; 368 | return menu.concat(cookBookBatMenu()); 369 | } 370 | 371 | function shotglassMenu() { 372 | return menu().filter((menuItem) => menuItem.size === 1 && menuItem.organ === "booze"); 373 | } 374 | 375 | function consumeDiet(diet: Diet, mpa: number) { 376 | const plannedDietEntries = diet.entries.sort( 377 | (a, b) => itemPriority(b.menuItems) - itemPriority(a.menuItems) 378 | ); 379 | 380 | print(`Diet Plan:`); 381 | for (const dietEntry of plannedDietEntries) { 382 | print(`${dietEntry.target()} ${dietEntry.helpers().join(",")}`); 383 | } 384 | 385 | while (sumNumbers(plannedDietEntries.map((e) => e.quantity)) > 0) { 386 | let progressed = false; 387 | for (const dietEntry of plannedDietEntries) { 388 | let quantity = dietEntry.quantity; 389 | 390 | // Compute the usable quantity of the diet entry 391 | const organ = dietEntry.target().organ ?? itemType(dietEntry.target().item); 392 | if (organ === "food") { 393 | quantity = clamp(Math.floor(getRemainingStomach() / dietEntry.target().size), 0, quantity); 394 | } else if (organ === "booze") { 395 | quantity = clamp(Math.floor(getRemainingLiver() / dietEntry.target().size), 0, quantity); 396 | if ( 397 | dietEntry.target().size === 1 && 398 | !get("_mimeArmyShotglassUsed") && 399 | have($item`mime army shotglass`) && 400 | quantity === 0 401 | ) { 402 | quantity = 1; 403 | } 404 | } else if (organ === "spleen item") { 405 | quantity = clamp(Math.floor(getRemainingSpleen() / dietEntry.target().size), 0, quantity); 406 | } 407 | const clean = spleenCleaners.get(dietEntry.target().item); 408 | if (clean) { 409 | quantity = clamp(Math.floor(mySpleenUse() / clean), 0, quantity); 410 | } 411 | 412 | if (quantity > 0) { 413 | progressed = true; 414 | for (const menuItem of dietEntry.menuItems) { 415 | if (menuItem.effect === $effect`Refined Palate`) { 416 | cliExecute(`genie effect ${menuItem.effect}`); 417 | } else { 418 | consumeSafe(quantity, menuItem.item, mpa, menuItem.data); 419 | } 420 | } 421 | dietEntry.quantity -= quantity; 422 | } 423 | } 424 | 425 | if (!progressed) throw `Unable to determine what to consume next`; 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/engine/resources.ts: -------------------------------------------------------------------------------- 1 | import { OutfitSpec } from "grimoire-kolmafia"; 2 | import { 3 | bjornifyFamiliar, 4 | buy, 5 | cliExecute, 6 | Familiar, 7 | familiarWeight, 8 | floor, 9 | Item, 10 | itemAmount, 11 | mallPrice, 12 | Monster, 13 | myLevel, 14 | myTurncount, 15 | retrieveItem, 16 | Skill, 17 | totalTurnsPlayed, 18 | use, 19 | weightAdjustment, 20 | } from "kolmafia"; 21 | import { 22 | $effect, 23 | $familiar, 24 | $item, 25 | $items, 26 | $monster, 27 | $skill, 28 | AsdonMartin, 29 | ensureEffect, 30 | get, 31 | getBanishedMonsters, 32 | getKramcoWandererChance, 33 | getModifier, 34 | have, 35 | Macro, 36 | sum, 37 | } from "libram"; 38 | import { debug } from "../lib"; 39 | 40 | export interface Resource { 41 | name: string; 42 | available: () => boolean; 43 | prepare?: () => void; 44 | equip?: Item | Familiar | Item[] | OutfitSpec; 45 | chance?: () => number; 46 | } 47 | 48 | export interface CombatResource extends Resource { 49 | do: Item | Skill | Macro; 50 | } 51 | 52 | export interface BanishSource extends CombatResource { 53 | do: Item | Skill; 54 | } 55 | 56 | export const banishSources: BanishSource[] = [ 57 | { 58 | name: "Bowl Curveball", 59 | available: () => have($item`cosmic bowling ball`), 60 | do: $skill`Bowl a Curveball`, 61 | }, 62 | { 63 | name: "Asdon Martin", 64 | available: (): boolean => { 65 | // From libram 66 | if (!AsdonMartin.installed()) return false; 67 | const banishes = get("banishedMonsters").split(":"); 68 | const bumperIndex = banishes 69 | .map((string) => string.toLowerCase()) 70 | .indexOf("spring-loaded front bumper"); 71 | if (bumperIndex === -1) return true; 72 | return myTurncount() - parseInt(banishes[bumperIndex + 1]) > 30; 73 | }, 74 | prepare: () => AsdonMartin.fillTo(50), 75 | do: $skill`Asdon Martin: Spring-Loaded Front Bumper`, 76 | }, 77 | { 78 | name: "Feel Hatred", 79 | available: () => get("_feelHatredUsed") < 3 && have($skill`Emotionally Chipped`), 80 | do: $skill`Feel Hatred`, 81 | }, 82 | { 83 | name: "Reflex Hammer", 84 | available: () => get("_reflexHammerUsed") < 3 && have($item`Lil' Doctor™ bag`), 85 | do: $skill`Reflex Hammer`, 86 | equip: $item`Lil' Doctor™ bag`, 87 | }, 88 | { 89 | name: "Snokebomb", 90 | available: () => get("_snokebombUsed") < 3 && have($skill`Snokebomb`), 91 | do: $skill`Snokebomb`, 92 | }, 93 | { 94 | name: "KGB dart", 95 | available: () => 96 | get("_kgbTranquilizerDartUses") < 3 && have($item`Kremlin's Greatest Briefcase`), 97 | do: $skill`KGB tranquilizer dart`, 98 | equip: $item`Kremlin's Greatest Briefcase`, 99 | }, 100 | { 101 | name: "Latte", 102 | available: () => 103 | (!get("_latteBanishUsed") || get("_latteRefillsUsed") < 2) && // Save one refil for aftercore 104 | have($item`latte lovers member's mug`), 105 | prepare: (): void => { 106 | if (get("_latteBanishUsed")) cliExecute("latte refill cinnamon pumpkin vanilla"); // Always unlocked 107 | }, 108 | do: $skill`Throw Latte on Opponent`, 109 | equip: $item`latte lovers member's mug`, 110 | }, 111 | { 112 | name: "Middle Finger", 113 | available: () => !get("_mafiaMiddleFingerRingUsed") && have($item`mafia middle finger ring`), 114 | do: $skill`Show them your ring`, 115 | equip: $item`mafia middle finger ring`, 116 | }, 117 | // If needed, use banishers from the mall 118 | { 119 | name: "Louder Than Bomb", 120 | prepare: () => { 121 | retrieveItem($item`Louder Than Bomb`); 122 | }, 123 | available: () => true, 124 | do: $item`Louder Than Bomb`, 125 | }, 126 | { 127 | name: "Tennis Ball", 128 | prepare: () => { 129 | retrieveItem($item`tennis ball`); 130 | }, 131 | available: () => true, 132 | do: $item`tennis ball`, 133 | }, 134 | { 135 | name: "Divine Champagne Popper", 136 | prepare: () => { 137 | retrieveItem($item`divine champagne popper`); 138 | }, 139 | available: () => true, 140 | do: $item`divine champagne popper`, 141 | }, 142 | // Turn-taking banishes: lowest priority 143 | { 144 | name: "Crystal Skull", 145 | prepare: () => { 146 | retrieveItem($item`crystal skull`); 147 | }, 148 | available: () => true, 149 | do: $item`crystal skull`, 150 | }, 151 | ]; 152 | 153 | export function unusedBanishes(to_banish: Monster[]): BanishSource[] { 154 | const used_banishes: Set = new Set(); 155 | const already_banished = new Map( 156 | Array.from(getBanishedMonsters(), (entry) => [entry[1], entry[0]]) 157 | ); 158 | 159 | // Record monsters that still need to be banished, and the banishes used 160 | const not_yet_banished: Monster[] = []; 161 | to_banish.forEach((monster) => { 162 | const banished_with = already_banished.get(monster); 163 | if (banished_with === undefined) { 164 | not_yet_banished.push(monster); 165 | } else { 166 | used_banishes.add(banished_with); 167 | // Map strange banish tracking to our resources 168 | if (banished_with === $item`training scroll: Snokebomb`) 169 | used_banishes.add($skill`Snokebomb`); 170 | if (banished_with === $item`tomayohawk-style reflex hammer`) 171 | used_banishes.add($skill`Reflex Hammer`); 172 | } 173 | }); 174 | if (not_yet_banished.length === 0) return []; // All monsters banished. 175 | 176 | debug(`Banish targets: ${not_yet_banished.join(", ")}`); 177 | debug(`Banishes used: ${Array.from(used_banishes).join(", ")}`); 178 | return banishSources.filter((banish) => banish.available() && !used_banishes.has(banish.do)); 179 | } 180 | 181 | export interface WandererSource extends Resource { 182 | monsters: Monster[]; 183 | chance: () => number; 184 | macro?: Macro; 185 | } 186 | 187 | export const wandererSources: WandererSource[] = [ 188 | { 189 | name: "Voted Legs", 190 | available: () => 191 | have($item`"I Voted!" sticker`) && 192 | totalTurnsPlayed() % 11 === 1 && 193 | get("lastVoteMonsterTurn") < totalTurnsPlayed() && 194 | get("_voteFreeFights") < 3 && 195 | myLevel() >= 10 && 196 | have($item`mutant legs`), 197 | equip: $items`"I Voted!" sticker, mutant legs`, 198 | monsters: [ 199 | $monster`government bureaucrat`, 200 | $monster`terrible mutant`, 201 | $monster`angry ghost`, 202 | $monster`annoyed snake`, 203 | $monster`slime blob`, 204 | ], 205 | chance: () => 1, // when available 206 | }, 207 | { 208 | name: "Voted Arm", 209 | available: () => 210 | have($item`"I Voted!" sticker`) && 211 | totalTurnsPlayed() % 11 === 1 && 212 | get("lastVoteMonsterTurn") < totalTurnsPlayed() && 213 | get("_voteFreeFights") < 3 && 214 | myLevel() >= 10 && 215 | have($item`mutant arm`), 216 | equip: $items`"I Voted!" sticker, mutant arm`, 217 | monsters: [ 218 | $monster`government bureaucrat`, 219 | $monster`terrible mutant`, 220 | $monster`angry ghost`, 221 | $monster`annoyed snake`, 222 | $monster`slime blob`, 223 | ], 224 | chance: () => 1, // when available 225 | }, 226 | { 227 | name: "Voted", 228 | available: () => 229 | have($item`"I Voted!" sticker`) && 230 | totalTurnsPlayed() % 11 === 1 && 231 | get("lastVoteMonsterTurn") < totalTurnsPlayed() && 232 | get("_voteFreeFights") < 3 && 233 | myLevel() >= 10, 234 | equip: $item`"I Voted!" sticker`, 235 | monsters: [ 236 | $monster`government bureaucrat`, 237 | $monster`terrible mutant`, 238 | $monster`angry ghost`, 239 | $monster`annoyed snake`, 240 | $monster`slime blob`, 241 | ], 242 | chance: () => 1, // when available 243 | }, 244 | { 245 | name: "Cursed Magnifying Glass", 246 | available: () => 247 | have($item`cursed magnifying glass`) && 248 | get("_voidFreeFights") < 5 && 249 | get("cursedMagnifyingGlassCount") >= 13, 250 | equip: $item`cursed magnifying glass`, 251 | monsters: [$monster`void guy`, $monster`void slab`, $monster`void spider`], 252 | chance: () => 1, // when available 253 | }, 254 | { 255 | name: "Goth", 256 | available: () => have($familiar`Artistic Goth Kid`) && get("_hipsterAdv") < 7, 257 | equip: $familiar`Artistic Goth Kid`, 258 | monsters: [ 259 | $monster`Black Crayon Beast`, 260 | $monster`Black Crayon Beetle`, 261 | $monster`Black Crayon Constellation`, 262 | $monster`Black Crayon Golem`, 263 | $monster`Black Crayon Demon`, 264 | $monster`Black Crayon Man`, 265 | $monster`Black Crayon Elemental`, 266 | $monster`Black Crayon Crimbo Elf`, 267 | $monster`Black Crayon Fish`, 268 | $monster`Black Crayon Goblin`, 269 | $monster`Black Crayon Hippy`, 270 | $monster`Black Crayon Hobo`, 271 | $monster`Black Crayon Shambling Monstrosity`, 272 | $monster`Black Crayon Manloid`, 273 | $monster`Black Crayon Mer-kin`, 274 | $monster`Black Crayon Frat Orc`, 275 | $monster`Black Crayon Penguin`, 276 | $monster`Black Crayon Pirate`, 277 | $monster`Black Crayon Flower`, 278 | $monster`Black Crayon Slime`, 279 | $monster`Black Crayon Undead Thing`, 280 | $monster`Black Crayon Spiraling Shape`, 281 | ], 282 | chance: () => [0.5, 0.4, 0.3, 0.2, 0.1, 0.1, 0.1, 0][get("_hipsterAdv")], 283 | }, 284 | { 285 | name: "Hipster", 286 | available: () => have($familiar`Mini-Hipster`) && get("_hipsterAdv") < 7, 287 | equip: $familiar`Mini-Hipster`, 288 | monsters: [ 289 | $monster`angry bassist`, 290 | $monster`blue-haired girl`, 291 | $monster`evil ex-girlfriend`, 292 | $monster`peeved roommate`, 293 | $monster`random scenester`, 294 | ], 295 | chance: () => [0.5, 0.4, 0.3, 0.2, 0.1, 0.1, 0.1, 0][get("_hipsterAdv")], 296 | }, 297 | { 298 | name: "Kramco (Drones)", 299 | available: () => 300 | have($item`Kramco Sausage-o-Matic™`) && 301 | myLevel() >= 10 && 302 | have($familiar`Grey Goose`) && 303 | familiarWeight($familiar`Grey Goose`) >= 6 && 304 | getKramcoWandererChance() === 1, 305 | equip: { 306 | offhand: $item`Kramco Sausage-o-Matic™`, 307 | familiar: $familiar`Grey Goose`, 308 | // Get 11 famexp at the end of the fight, to maintain goose weight 309 | weapon: $item`yule hatchet`, 310 | famequip: $item`grey down vest`, 311 | acc1: $item`teacher's pen`, 312 | acc2: $item`teacher's pen`, 313 | acc3: $item`teacher's pen`, 314 | }, 315 | monsters: [$monster`sausage goblin`], 316 | chance: () => getKramcoWandererChance(), 317 | macro: new Macro().trySkill($skill`Emit Matter Duplicating Drones`), 318 | }, 319 | { 320 | name: "Kramco", 321 | available: () => have($item`Kramco Sausage-o-Matic™`) && myLevel() >= 10, 322 | equip: $item`Kramco Sausage-o-Matic™`, 323 | monsters: [$monster`sausage goblin`], 324 | chance: () => getKramcoWandererChance(), 325 | }, 326 | ]; 327 | 328 | export function canChargeVoid(): boolean { 329 | return get("_voidFreeFights") < 5 && get("cursedMagnifyingGlassCount") < 13; 330 | } 331 | 332 | export interface RunawaySource extends CombatResource { 333 | do: Macro; 334 | banishes: boolean; 335 | chance: () => number; 336 | } 337 | 338 | // Gear and familiar to use for runaways (i.e., Bandersnatch or Stomping Boots) 339 | const familiarPants = 340 | $items`repaid diaper, Great Wolf's beastly trousers, Greaves of the Murk Lord`.find((item) => 341 | have(item) 342 | ); 343 | const familiarEquip = have($item`astral pet sweater`) 344 | ? $item`astral pet sweater` 345 | : have($familiar`Cornbeefadon`) 346 | ? $item`amulet coin` 347 | : have($familiar`Mu`) 348 | ? $item`luck incense` 349 | : null; 350 | const familiarGear = [ 351 | ...$items`Daylight Shavings Helmet, Buddy Bjorn, Stephen's lab coat, hewn moon-rune spoon`, 352 | ...(familiarEquip ? [familiarEquip] : []), 353 | ...(familiarPants ? [familiarPants] : []), 354 | ]; 355 | const familiarGearBonus = 356 | 5 + sum(familiarGear, (item: Item) => getModifier("Familiar Weight", item)); 357 | const familiarEffectBonus = 15; 358 | const runawayFamiliar = have($familiar`Frumious Bandersnatch`) 359 | ? $familiar`Frumious Bandersnatch` 360 | : have($familiar`Pair of Stomping Boots`) 361 | ? $familiar`Pair of Stomping Boots` 362 | : $familiar`none`; 363 | 364 | function availableFamiliarRunaways(otherBonus: number) { 365 | if (runawayFamiliar === $familiar`none`) return 0; 366 | return floor( 367 | (familiarWeight(runawayFamiliar) + 368 | familiarEffectBonus + 369 | familiarGearBonus + 370 | otherBonus + 371 | (have($effect`Open Heart Surgery`) ? 10 : 0)) / 372 | 5 373 | ); 374 | } 375 | 376 | export const runawayValue = 377 | have($item`Greatest American Pants`) || have($item`navel ring of navel gazing`) 378 | ? 0.8 * get("valueOfAdventure") 379 | : get("valueOfAdventure"); 380 | 381 | export const runawaySources: RunawaySource[] = [ 382 | { 383 | name: "Bowl Curveball", 384 | available: () => have($item`cosmic bowling ball`), 385 | do: new Macro().skill($skill`Bowl a Curveball`), 386 | chance: () => 1, 387 | banishes: true, 388 | }, 389 | { 390 | name: "Asdon Martin", 391 | available: (): boolean => { 392 | // From libram 393 | if (!AsdonMartin.installed()) return false; 394 | const banishes = get("banishedMonsters").split(":"); 395 | const bumperIndex = banishes 396 | .map((string) => string.toLowerCase()) 397 | .indexOf("spring-loaded front bumper"); 398 | if (bumperIndex === -1) return true; 399 | return myTurncount() - parseInt(banishes[bumperIndex + 1]) > 30; 400 | }, 401 | prepare: () => AsdonMartin.fillTo(50), 402 | do: new Macro().skill($skill`Asdon Martin: Spring-Loaded Front Bumper`), 403 | chance: () => 1, 404 | banishes: true, 405 | }, 406 | { 407 | name: "Familiar Runaways", 408 | available: () => 409 | runawayFamiliar !== $familiar`none` && 410 | have(runawayFamiliar) && 411 | availableFamiliarRunaways(5) > get("_banderRunaways"), // 5 from iFlail 412 | prepare: (): void => { 413 | bjornifyFamiliar($familiar`Gelatinous Cubeling`); 414 | if ( 415 | floor((familiarWeight(runawayFamiliar) + weightAdjustment()) / 5) <= get("_banderRunaways") 416 | ) { 417 | throw `Trying to use Bandersnatch or Stomping Boots, but weight was overestimated.`; 418 | } 419 | if (runawayFamiliar === $familiar`Frumious Bandersnatch`) { 420 | ensureEffect($effect`Ode to Booze`, 5); 421 | } 422 | }, 423 | equip: { 424 | familiar: runawayFamiliar, 425 | equip: [...familiarGear, $item`iFlail`], 426 | }, 427 | do: new Macro().runaway(), 428 | chance: () => 1, 429 | banishes: false, 430 | }, 431 | { 432 | name: "Familiar Runaways (with offhand)", // Use the potted plant as long as possible 433 | available: () => 434 | runawayFamiliar !== $familiar`none` && 435 | have(runawayFamiliar) && 436 | availableFamiliarRunaways(10) > get("_banderRunaways"), // 10 from iFlails 437 | prepare: (): void => { 438 | bjornifyFamiliar($familiar`Gelatinous Cubeling`); 439 | if ( 440 | floor((familiarWeight(runawayFamiliar) + weightAdjustment()) / 5) <= get("_banderRunaways") 441 | ) { 442 | throw `Trying to use last Bandersnatch or Stomping Boots, but weight was overestimated.`; 443 | } 444 | if (runawayFamiliar === $familiar`Frumious Bandersnatch`) { 445 | ensureEffect($effect`Ode to Booze`, 5); 446 | } 447 | }, 448 | equip: { 449 | familiar: runawayFamiliar, 450 | equip: [...familiarGear, $item`iFlail`, $item`familiar scrapbook`], 451 | }, 452 | do: new Macro().runaway(), 453 | chance: () => 1, 454 | banishes: false, 455 | }, 456 | { 457 | name: "Blank-Out", 458 | prepare: (): void => { 459 | if (!have($item`glob of Blank-Out`)) { 460 | if (!have($item`bottle of Blank-Out`)) { 461 | buy(1, $item`bottle of Blank-Out`, 5 * runawayValue); 462 | } 463 | use($item`bottle of Blank-Out`); 464 | } 465 | }, 466 | available: () => 467 | have($item`glob of Blank-Out`) || 468 | (mallPrice($item`bottle of Blank-Out`) < 5 * runawayValue && !get("_blankoutUsed")), 469 | do: new Macro().tryItem($item`glob of Blank-Out`), 470 | chance: () => 1, 471 | banishes: false, 472 | }, 473 | { 474 | name: "GAP", 475 | available: () => have($item`Greatest American Pants`), 476 | equip: $item`Greatest American Pants`, 477 | do: new Macro().runaway(), 478 | chance: () => (get("_navelRunaways") < 3 ? 1 : 0.2), 479 | banishes: false, 480 | }, 481 | { 482 | name: "Navel Ring", 483 | available: () => have($item`navel ring of navel gazing`), 484 | equip: $item`navel ring of navel gazing`, 485 | do: new Macro().runaway(), 486 | chance: () => (get("_navelRunaways") < 3 ? 1 : 0.2), 487 | banishes: false, 488 | }, 489 | { 490 | name: "Peppermint Parasol", 491 | available: () => 492 | have($item`peppermint parasol`) || 493 | mallPrice($item`peppermint parasol`) < 10 * get("valueOfAdventure"), 494 | prepare: () => { 495 | if (have($item`peppermint parasol`)) return; 496 | if (itemAmount($item`peppermint sprout`) >= 5) { 497 | retrieveItem($item`peppermint parasol`); 498 | } else if (mallPrice($item`peppermint parasol`) < 5 * mallPrice($item`peppermint sprout`)) { 499 | buy($item`peppermint parasol`, 1, mallPrice($item`peppermint parasol`)); 500 | } else { 501 | buy( 502 | $item`peppermint sprout`, 503 | 5 - itemAmount($item`peppermint sprout`), 504 | mallPrice($item`peppermint sprout`) 505 | ); 506 | retrieveItem($item`peppermint parasol`); 507 | } 508 | }, 509 | do: new Macro().item($item`peppermint parasol`), 510 | chance: () => (get("_navelRunaways") < 3 ? 1 : 0.2), 511 | banishes: false, 512 | }, 513 | ]; 514 | 515 | export interface FreekillSource extends CombatResource { 516 | do: Item | Skill; 517 | } 518 | 519 | export const freekillSources: FreekillSource[] = [ 520 | { 521 | name: "Lil' Doctor™ bag", 522 | available: () => have($item`Lil' Doctor™ bag`) && get("_chestXRayUsed") < 3, 523 | do: $skill`Chest X-Ray`, 524 | equip: $item`Lil' Doctor™ bag`, 525 | }, 526 | { 527 | name: "Gingerbread Mob Hit", 528 | available: () => have($skill`Gingerbread Mob Hit`) && !get("_gingerbreadMobHitUsed"), 529 | do: $skill`Gingerbread Mob Hit`, 530 | }, 531 | { 532 | name: "Shattering Punch", 533 | available: () => have($skill`Shattering Punch`) && get("_shatteringPunchUsed") < 3, 534 | do: $skill`Shattering Punch`, 535 | }, 536 | { 537 | name: "Replica bat-oomerang", 538 | available: () => have($item`replica bat-oomerang`) && get("_usedReplicaBatoomerang") < 3, 539 | do: $item`replica bat-oomerang`, 540 | }, 541 | { 542 | name: "The Jokester's gun", 543 | available: () => have($item`The Jokester's gun`) && !get("_firedJokestersGun"), 544 | do: $skill`Fire the Jokester's Gun`, 545 | equip: $item`The Jokester's gun`, 546 | }, 547 | { 548 | name: "Asdon Martin: Missile Launcher", 549 | available: () => AsdonMartin.installed() && !get("_missileLauncherUsed"), 550 | prepare: () => AsdonMartin.fillTo(100), 551 | do: $skill`Asdon Martin: Missile Launcher`, 552 | }, 553 | ]; 554 | -------------------------------------------------------------------------------- /src/tasks/misc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | adv1, 3 | cliExecute, 4 | expectedColdMedicineCabinet, 5 | familiarEquippedEquipment, 6 | familiarWeight, 7 | gamedayToInt, 8 | getProperty, 9 | getWorkshed, 10 | itemAmount, 11 | myBasestat, 12 | myPrimestat, 13 | retrieveItem, 14 | retrievePrice, 15 | runChoice, 16 | totalTurnsPlayed, 17 | use, 18 | useFamiliar, 19 | useSkill, 20 | visitUrl, 21 | } from "kolmafia"; 22 | import { 23 | $effect, 24 | $familiar, 25 | $familiars, 26 | $item, 27 | $items, 28 | $location, 29 | $monster, 30 | $skill, 31 | AutumnAton, 32 | Clan, 33 | get, 34 | getSaleValue, 35 | have, 36 | Macro, 37 | set, 38 | uneffect, 39 | } from "libram"; 40 | import { CombatStrategy } from "../engine/combat"; 41 | import { Quest } from "../engine/task"; 42 | import { OutfitSpec, step } from "grimoire-kolmafia"; 43 | import { args } from "../main"; 44 | 45 | export const MiscQuest: Quest = { 46 | name: "Misc", 47 | tasks: [ 48 | { 49 | name: "Unlock Beach", 50 | after: [], 51 | completed: () => have($item`bitchin' meatcar`) || have($item`Desert Bus pass`), 52 | do: () => cliExecute("acquire 1 bitchin' meatcar"), 53 | limit: { tries: 1 }, 54 | freeaction: true, 55 | }, 56 | { 57 | name: "Unlock Island", 58 | after: ["Mosquito/Start"], 59 | completed: () => 60 | have($item`dingy dinghy`) || 61 | have($item`junk junk`) || 62 | have($item`skeletal skiff`) || 63 | have($item`yellow submarine`), 64 | do: () => { 65 | const options = $items`skeletal skiff, yellow submarine`; 66 | const bestChoice = options.sort((a, b) => retrievePrice(a) - retrievePrice(b))[0]; 67 | if (bestChoice === $item`yellow submarine`) { 68 | // Open the mystic store if needed 69 | if (!have($item`continuum transfunctioner`)) { 70 | visitUrl("place.php?whichplace=forestvillage&action=fv_mystic"); 71 | runChoice(1); 72 | runChoice(1); 73 | runChoice(1); 74 | } 75 | } 76 | retrieveItem(bestChoice); 77 | }, 78 | limit: { tries: 1 }, 79 | freeaction: true, 80 | }, 81 | { 82 | name: "Floundry", 83 | after: [], 84 | completed: () => 85 | have($item`fish hatchet`) || get("_loop_casual_floundry_checked", "") === Clan.get().name, 86 | do: () => { 87 | const sufficientFish = visitUrl("clan_viplounge.php?action=floundry").match( 88 | "([0-9]+) hatchetfish" 89 | ); 90 | if (sufficientFish === null || parseInt(sufficientFish[1]) < 10) { 91 | // Recheck if the script is rerun with a new clan 92 | set("_loop_casual_floundry_checked", Clan.get().name); 93 | } else { 94 | cliExecute("acquire 1 fish hatchet"); 95 | } 96 | }, 97 | limit: { tries: 1 }, 98 | freeaction: true, 99 | }, 100 | { 101 | name: "Short Cook", 102 | after: [], 103 | ready: () => have($familiar`Shorter-Order Cook`), 104 | completed: () => 105 | familiarEquippedEquipment($familiar`Shorter-Order Cook`) === $item`blue plate`, 106 | acquire: [{ item: $item`blue plate` }], 107 | do: () => useFamiliar($familiar`Mosquito`), // Switch away to keep blue plate equipped 108 | outfit: { familiar: $familiar`Shorter-Order Cook`, equip: $items`blue plate` }, 109 | freeaction: true, 110 | limit: { tries: 1 }, 111 | }, 112 | { 113 | name: "Acquire Kgnee", 114 | after: [], 115 | ready: () => 116 | have($familiar`Reagnimated Gnome`) && 117 | !have($item`gnomish housemaid's kgnee`) && 118 | !get("_loopcasual_checkedGnome", false), 119 | completed: () => 120 | !have($familiar`Reagnimated Gnome`) || 121 | have($item`gnomish housemaid's kgnee`) || 122 | get("_loopcasual_checkedGnome", false), 123 | do: () => { 124 | visitUrl("arena.php"); 125 | runChoice(4); 126 | set("_loopcasual_checkedGnome", true); 127 | }, 128 | outfit: { familiar: $familiar`Reagnimated Gnome` }, 129 | limit: { tries: 1 }, 130 | }, 131 | { 132 | name: "Acquire FamEquip", 133 | after: [], 134 | ready: () => 135 | $items`astral pet sweater, amulet coin, luck incense`.some((item) => !have(item)) && 136 | $familiars`Mu, Cornbeefadon`.some(have), 137 | completed: () => 138 | $items`astral pet sweater, amulet coin, luck incense`.some((item) => have(item)) || 139 | !$familiars`Mu, Cornbeefadon`.some(have), 140 | do: () => { 141 | const famToUse = $familiars`Mu, Cornbeefadon`.find(have); 142 | if (famToUse) { 143 | useFamiliar(famToUse); 144 | retrieveItem($item`box of Familiar Jacks`); 145 | use($item`box of Familiar Jacks`); 146 | } 147 | }, 148 | limit: { tries: 1 }, 149 | }, 150 | { 151 | name: "Voting", 152 | after: [], 153 | ready: () => get("voteAlways"), 154 | completed: () => have($item`"I Voted!" sticker`) || get("_voteToday"), 155 | do: (): void => { 156 | // Taken from garbo 157 | const voterValueTable = [ 158 | { 159 | monster: $monster`terrible mutant`, 160 | value: getSaleValue($item`glob of undifferentiated tissue`) + 10, 161 | }, 162 | { 163 | monster: $monster`angry ghost`, 164 | value: getSaleValue($item`ghostly ectoplasm`) * 1.11, 165 | }, 166 | { 167 | monster: $monster`government bureaucrat`, 168 | value: getSaleValue($item`absentee voter ballot`) * 0.05 + 75 * 0.25 + 50, 169 | }, 170 | { 171 | monster: $monster`annoyed snake`, 172 | value: gamedayToInt(), 173 | }, 174 | { 175 | monster: $monster`slime blob`, 176 | value: 95 - gamedayToInt(), 177 | }, 178 | ]; 179 | 180 | visitUrl("place.php?whichplace=town_right&action=townright_vote"); 181 | 182 | const votingMonsterPriority = voterValueTable 183 | .sort((a, b) => b.value - a.value) 184 | .map((element) => element.monster.name); 185 | 186 | const initPriority = new Map([ 187 | ["Meat Drop: +30", 10], 188 | ["Item Drop: +15", 9], 189 | ["Familiar Experience: +2", 8], 190 | ["Adventures: +1", 7], 191 | ["Monster Level: +10", 5], 192 | [`${myPrimestat()} Percent: +25`, 3], 193 | [`Experience (${myPrimestat()}): +4`, 2], 194 | ["Meat Drop: -30", -2], 195 | ["Item Drop: -15", -2], 196 | ["Familiar Experience: -2", -2], 197 | ]); 198 | 199 | const monsterVote = 200 | votingMonsterPriority.indexOf(get("_voteMonster1")) < 201 | votingMonsterPriority.indexOf(get("_voteMonster2")) 202 | ? 1 203 | : 2; 204 | 205 | const voteLocalPriorityArr = [ 206 | [ 207 | 0, 208 | initPriority.get(get("_voteLocal1")) || 209 | (get("_voteLocal1").indexOf("-") === -1 ? 1 : -1), 210 | ], 211 | [ 212 | 1, 213 | initPriority.get(get("_voteLocal2")) || 214 | (get("_voteLocal2").indexOf("-") === -1 ? 1 : -1), 215 | ], 216 | [ 217 | 2, 218 | initPriority.get(get("_voteLocal3")) || 219 | (get("_voteLocal3").indexOf("-") === -1 ? 1 : -1), 220 | ], 221 | [ 222 | 3, 223 | initPriority.get(get("_voteLocal4")) || 224 | (get("_voteLocal4").indexOf("-") === -1 ? 1 : -1), 225 | ], 226 | ]; 227 | 228 | const bestVotes = voteLocalPriorityArr.sort((a, b) => b[1] - a[1]); 229 | const firstInit = bestVotes[0][0]; 230 | const secondInit = bestVotes[1][0]; 231 | 232 | visitUrl( 233 | `choice.php?option=1&whichchoice=1331&g=${monsterVote}&local[]=${firstInit}&local[]=${secondInit}` 234 | ); 235 | }, 236 | limit: { tries: 1 }, 237 | freeaction: true, 238 | }, 239 | { 240 | name: "Protonic Ghost", 241 | after: [], 242 | completed: () => step("questL13Final") >= 0, // Stop after tower starts 243 | ready: () => { 244 | if (!have($item`protonic accelerator pack`)) return false; 245 | if (get("questPAGhost") === "unstarted") return false; 246 | switch (get("ghostLocation")) { 247 | case $location`Cobb's Knob Treasury`: 248 | return step("questL05Goblin") >= 1; 249 | case $location`The Haunted Conservatory`: 250 | return step("questM20Necklace") >= 0; 251 | case $location`The Haunted Gallery`: 252 | return step("questM21Dance") >= 1; 253 | case $location`The Haunted Kitchen`: 254 | return step("questM20Necklace") >= 0; 255 | case $location`The Haunted Wine Cellar`: 256 | return step("questL11Manor") >= 1; 257 | case $location`The Icy Peak`: 258 | return step("questL08Trapper") === 999; 259 | case $location`Inside the Palindome`: 260 | return have($item`Talisman o' Namsilat`); 261 | case $location`The Old Landfill`: 262 | return myBasestat(myPrimestat()) >= 25 && step("questL02Larva") >= 0; 263 | case $location`Madness Bakery`: 264 | case $location`The Overgrown Lot`: 265 | case $location`The Skeleton Store`: 266 | return true; // Can freely start quest 267 | case $location`The Smut Orc Logging Camp`: 268 | return step("questL09Topping") >= 0; 269 | case $location`The Spooky Forest`: 270 | return step("questL02Larva") >= 0; 271 | } 272 | return false; 273 | }, 274 | prepare: () => { 275 | // Start quests if needed 276 | switch (get("ghostLocation")) { 277 | case $location`Madness Bakery`: 278 | if (step("questM25Armorer") === -1) { 279 | visitUrl("shop.php?whichshop=armory"); 280 | visitUrl("shop.php?whichshop=armory&action=talk"); 281 | visitUrl("choice.php?pwd=&whichchoice=1065&option=1"); 282 | } 283 | return; 284 | case $location`The Old Landfill`: 285 | if (step("questM19Hippy") === -1) { 286 | visitUrl("place.php?whichplace=woods&action=woods_smokesignals"); 287 | visitUrl("choice.php?pwd=&whichchoice=798&option=1"); 288 | visitUrl("choice.php?pwd=&whichchoice=798&option=2"); 289 | visitUrl("woods.php"); 290 | } 291 | return; 292 | case $location`The Overgrown Lot`: 293 | if (step("questM24Doc") === -1) { 294 | visitUrl("shop.php?whichshop=doc"); 295 | visitUrl("shop.php?whichshop=doc&action=talk"); 296 | runChoice(1); 297 | } 298 | return; 299 | case $location`The Skeleton Store`: 300 | if (step("questM23Meatsmith") === -1) { 301 | visitUrl("shop.php?whichshop=meatsmith"); 302 | visitUrl("shop.php?whichshop=meatsmith&action=talk"); 303 | runChoice(1); 304 | } 305 | return; 306 | default: 307 | return; 308 | } 309 | }, 310 | do: () => { 311 | adv1(get("ghostLocation") ?? $location`none`, 0, ""); 312 | }, 313 | outfit: (): OutfitSpec => { 314 | if (get("ghostLocation") === $location`Inside the Palindome`) 315 | return { equip: $items`Talisman o' Namsilat, protonic accelerator pack` }; 316 | return { equip: $items`protonic accelerator pack` }; 317 | }, 318 | combat: new CombatStrategy().macro( 319 | new Macro() 320 | .skill($skill`Shoot Ghost`) 321 | .skill($skill`Shoot Ghost`) 322 | .skill($skill`Shoot Ghost`) 323 | .skill($skill`Trap Ghost`) 324 | ), 325 | limit: { tries: 10 }, 326 | }, 327 | { 328 | name: "CMC Pills", 329 | ready: () => 330 | getWorkshed() === $item`cold medicine cabinet` && 331 | (get("_coldMedicineConsults") === 0 || 332 | totalTurnsPlayed() >= get("_nextColdMedicineConsult")) && 333 | $items`Extrovermectin™`.includes(expectedColdMedicineCabinet().pill), 334 | completed: () => get("_coldMedicineConsults") >= 5, 335 | priority: () => true, 336 | do: () => cliExecute("cmc pill"), 337 | limit: { tries: 5 }, 338 | }, 339 | { 340 | name: "Autumn-aton", 341 | after: [], 342 | ready: () => have($item`autumn-aton`), 343 | completed: () => step("questL13Final") >= 0, 344 | priority: () => true, 345 | combat: new CombatStrategy().macro(new Macro()).kill(), 346 | do: () => { 347 | //make sure all available upgrades are installed 348 | AutumnAton.upgrade(); 349 | //get upgrades 350 | if (!AutumnAton.currentUpgrades().includes("leftleg1")) { 351 | AutumnAton.sendTo($location`Noob Cave`); 352 | } else if ( 353 | !AutumnAton.currentUpgrades().includes("rightleg1") && 354 | AutumnAton.availableLocations().includes($location`The Haunted Kitchen`) 355 | ) { 356 | AutumnAton.sendTo($location`The Haunted Kitchen`); 357 | } else if (!AutumnAton.currentUpgrades().includes("leftarm1")) { 358 | AutumnAton.sendTo($location`The Haunted Pantry`); 359 | } else if ( 360 | !AutumnAton.currentUpgrades().includes("rightarm1") && 361 | AutumnAton.availableLocations().includes($location`Twin Peak`) 362 | ) { 363 | AutumnAton.sendTo($location`Twin Peak`); 364 | } 365 | //lighthouse 366 | else if ( 367 | AutumnAton.currentUpgrades().length >= 4 && 368 | step("questL12War") >= 1 && 369 | itemAmount($item`barrel of gunpowder`) < 5 && 370 | get("sidequestLighthouseCompleted") === "none" 371 | ) { 372 | adv1($location`Sonofa Beach`); 373 | AutumnAton.sendTo($location`Sonofa Beach`); 374 | } 375 | //farming 376 | else if (AutumnAton.availableLocations().includes($location`The Defiled Nook`)) { 377 | AutumnAton.sendTo($location`The Defiled Nook`); 378 | } 379 | //If all else fails, grab an autumn leaf. This shouldn't ever happen 380 | else { 381 | AutumnAton.sendTo($location`The Sleazy Back Alley`); 382 | } 383 | }, 384 | limit: { tries: 15 }, 385 | }, 386 | { 387 | name: "Goose Exp", 388 | after: [], 389 | priority: () => true, 390 | completed: () => 391 | familiarWeight($familiar`Grey Goose`) >= 9 || 392 | get("_loop_casual_chef_goose") === "true" || 393 | !have($familiar`Grey Goose`) || 394 | !have($familiar`Shorter-Order Cook`), 395 | do: () => { 396 | set("_loop_casual_chef_goose", "true"); 397 | }, 398 | outfit: { familiar: $familiar`Grey Goose` }, 399 | limit: { tries: 1 }, 400 | freeaction: true, 401 | }, 402 | { 403 | name: "Workshed", 404 | after: [], 405 | priority: () => true, 406 | completed: () => getWorkshed() !== $item`none` || !have(args.workshed), 407 | do: () => use(args.workshed), 408 | limit: { tries: 1 }, 409 | freeaction: true, 410 | }, 411 | { 412 | name: "Open McHugeLarge Bag", 413 | after: [], 414 | priority: () => true, 415 | completed: () => 416 | // eslint-disable-next-line libram/verify-constants 417 | !have($item`McHugeLarge duffel bag`) || have($item`McHugeLarge right pole`), 418 | // eslint-disable-next-line libram/verify-constants 419 | do: () => visitUrl("inventory.php?action=skiduffel&pwd"), 420 | freeaction: true, 421 | limit: { tries: 1 }, 422 | }, 423 | ], 424 | }; 425 | 426 | function keyCount(): number { 427 | let count = itemAmount($item`fat loot token`); 428 | if (have($item`Boris's key`) || get("nsTowerDoorKeysUsed").includes("Boris")) count++; 429 | if (have($item`Jarlsberg's key`) || get("nsTowerDoorKeysUsed").includes("Jarlsberg")) count++; 430 | if (have($item`Sneaky Pete's key`) || get("nsTowerDoorKeysUsed").includes("Sneaky Pete")) count++; 431 | return count; 432 | } 433 | export const KeysQuest: Quest = { 434 | name: "Keys", 435 | tasks: [ 436 | { 437 | name: "Deck", 438 | after: [], 439 | completed: () => get("_deckCardsDrawn") > 0, 440 | do: () => cliExecute("cheat tower"), 441 | limit: { tries: 1 }, 442 | freeaction: true, 443 | }, 444 | { 445 | name: "Lockpicking", 446 | after: ["Deck"], 447 | completed: () => !have($skill`Lock Picking`) || get("lockPicked"), 448 | do: (): void => { 449 | useSkill($skill`Lock Picking`); 450 | }, 451 | choices: { 452 | 1414: (): number => { 453 | if (!have($item`Boris's key`)) return 1; 454 | else if (!have($item`Jarlsberg's key`)) return 2; 455 | else return 3; 456 | }, 457 | }, 458 | limit: { tries: 1 }, 459 | freeaction: true, 460 | }, 461 | { 462 | name: "Malware", 463 | after: [], 464 | acquire: [ 465 | { item: $item`daily dungeon malware` }, 466 | { item: $item`Pick-O-Matic lockpicks` }, 467 | { item: $item`eleven-foot pole` }, 468 | ], 469 | completed: () => 470 | get("_dailyDungeonMalwareUsed") || get("dailyDungeonDone") || keyCount() >= 3, 471 | prepare: () => { 472 | set("_loop_casual_malware_amount", itemAmount($item`daily dungeon malware`)); 473 | }, 474 | do: $location`The Daily Dungeon`, 475 | post: () => { 476 | if (itemAmount($item`daily dungeon malware`) < get("_loop_casual_malware_amount", 0)) 477 | set("_dailyDungeonMalwareUsed", true); 478 | uneffect($effect`Apathy`); 479 | }, 480 | outfit: { 481 | equip: $items`ring of Detect Boring Doors`, 482 | avoid: $items`carnivorous potted plant`, 483 | }, 484 | combat: new CombatStrategy().macro( 485 | new Macro() 486 | .item($item`daily dungeon malware`) 487 | .attack() 488 | .repeat() 489 | ), 490 | choices: { 491 | 689: 1, 492 | 690: 2, 493 | 691: 3, // Do not skip the second chest; there is a chance we skip all the monsters 494 | 692: 3, 495 | 693: 2, 496 | }, 497 | limit: { soft: 11 }, 498 | }, 499 | { 500 | name: "Daily Dungeon", 501 | after: ["Deck", "Lockpicking", "Malware"], 502 | acquire: [{ item: $item`Pick-O-Matic lockpicks` }, { item: $item`eleven-foot pole` }], 503 | completed: () => get("dailyDungeonDone") || keyCount() >= 3, 504 | do: $location`The Daily Dungeon`, 505 | post: () => uneffect($effect`Apathy`), 506 | outfit: { 507 | equip: $items`ring of Detect Boring Doors`, 508 | }, 509 | combat: new CombatStrategy().kill(), 510 | choices: { 689: 1, 690: 2, 691: 2, 692: 3, 693: 2 }, 511 | limit: { tries: 11 }, 512 | }, 513 | { 514 | name: "Finish", 515 | after: ["Deck", "Lockpicking", "Malware", "Daily Dungeon"], 516 | completed: () => keyCount() >= 3, 517 | do: (): void => { 518 | throw "Unable to obtain enough fat loot tokens"; 519 | }, 520 | limit: { tries: 1 }, 521 | freeaction: true, 522 | }, 523 | ], 524 | }; 525 | 526 | export const DigitalQuest: Quest = { 527 | name: "Digital", 528 | tasks: [ 529 | { 530 | name: "Open", 531 | after: ["Mosquito/Start"], 532 | completed: () => have($item`continuum transfunctioner`), 533 | do: () => { 534 | visitUrl("place.php?whichplace=forestvillage&action=fv_mystic"); 535 | runChoice(1); 536 | runChoice(1); 537 | runChoice(1); 538 | }, 539 | limit: { tries: 1 }, 540 | freeaction: true, 541 | }, 542 | { 543 | name: "Fungus", 544 | after: ["Open"], 545 | completed: () => getScore() >= 10000, 546 | ready: () => get("8BitColor", "black") === "red", 547 | // eslint-disable-next-line libram/verify-constants 548 | do: $location`The Fungus Plains`, 549 | outfit: { modifier: "meat", equip: $items`continuum transfunctioner` }, 550 | combat: new CombatStrategy().kill(), 551 | limit: { tries: 21 }, 552 | delay: 5, 553 | }, 554 | { 555 | name: "Vanya", 556 | after: ["Open"], 557 | completed: () => getScore() >= 10000, 558 | ready: () => get("8BitColor", "black") === "black" || get("8BitColor", "black") === "", 559 | // eslint-disable-next-line libram/verify-constants 560 | do: $location`Vanya's Castle`, 561 | outfit: { modifier: "init", equip: $items`continuum transfunctioner` }, 562 | combat: new CombatStrategy().kill(), 563 | limit: { tries: 21 }, 564 | delay: 10, 565 | }, 566 | { 567 | name: "Megalo", 568 | after: ["Open"], 569 | completed: () => getScore() >= 10000, 570 | ready: () => get("8BitColor", "black") === "blue", 571 | // eslint-disable-next-line libram/verify-constants 572 | do: $location`Megalo-City`, 573 | outfit: { modifier: "DA", equip: $items`continuum transfunctioner` }, 574 | combat: new CombatStrategy().kill(), 575 | limit: { tries: 21 }, 576 | delay: 5, 577 | }, 578 | { 579 | name: "Hero", 580 | after: ["Open"], 581 | completed: () => getScore() >= 10000, 582 | ready: () => get("8BitColor", "black") === "green", 583 | // eslint-disable-next-line libram/verify-constants 584 | do: $location`Hero's Field`, 585 | outfit: { modifier: "item", equip: $items`continuum transfunctioner` }, 586 | combat: new CombatStrategy().kill(), 587 | limit: { tries: 21 }, 588 | delay: 5, 589 | }, 590 | { 591 | name: "Key", 592 | after: ["Open", "Fungus", "Vanya", "Megalo", "Hero"], 593 | completed: () => 594 | have($item`digital key`) || get("nsTowerDoorKeysUsed").includes("digital key"), 595 | do: () => { 596 | if (getScore() >= 10000) { 597 | visitUrl("place.php?whichplace=8bit&action=8treasure"); 598 | runChoice(1); 599 | } 600 | }, 601 | outfit: { equip: $items`continuum transfunctioner` }, 602 | limit: { tries: 2 }, // The first time may only set the property 603 | }, 604 | ], 605 | }; 606 | 607 | function getScore(): number { 608 | const score = getProperty("8BitScore"); 609 | if (score === "") return 0; 610 | return parseInt(score.replace(",", "")); 611 | } 612 | --------------------------------------------------------------------------------