From 77db00d2a8f45b9fbfe9eaa738067521e21610f6 Mon Sep 17 00:00:00 2001 From: Patrick Stalcup Date: Wed, 4 Oct 2023 09:17:46 -0400 Subject: [PATCH] Grimoirize Yachtzee --- README.md | 5 - mafia/data/garbo_settings.json | 5 - src/combat.ts | 3 +- src/config.ts | 7 - src/extrovermectin.ts | 7 +- src/fights.ts | 9 +- src/index.ts | 13 +- src/outfit/dropsgearAccessories.ts | 3 - src/post/index.ts | 29 +- src/potions.ts | 6 +- src/turns.ts | 19 +- src/yachtzee/buffs.ts | 256 --------- src/yachtzee/diet.ts | 866 ----------------------------- src/yachtzee/fishy.ts | 299 ---------- src/yachtzee/index.ts | 239 ++++---- src/yachtzee/lib.ts | 117 ---- src/yachtzee/outfit.ts | 100 ---- 17 files changed, 116 insertions(+), 1867 deletions(-) delete mode 100644 src/yachtzee/buffs.ts delete mode 100644 src/yachtzee/diet.ts delete mode 100644 src/yachtzee/fishy.ts delete mode 100644 src/yachtzee/lib.ts delete mode 100644 src/yachtzee/outfit.ts diff --git a/README.md b/README.md index 11e1b405f..36d075bff 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ - [`ascend` flag](#ascend-flag) - [`nodiet` flag](#nodiet-flag) - [`simdiet` flag](#simdiet-flag) - - [`yachtzeechain` flag](#yachtzeechain-flag) - [`workshed` arg](#workshed-arg) - [`quick` flag](#quick-flag) - [Turncount](#turncount) @@ -113,10 +112,6 @@ _EXPERIMENTAL_ Garbo will not eat or drink anything as part of the run (includin Garbo will list the optimal diet it plans to consume computed from your defined mpa and current prices, and then exit. -### `yachtzeechain` flag - -_EXPERIMENTAL_ Garbo will attempt to chain the Yachtzee! NC after all the free fights are completed, just before it attempts embezzlers. This command cannot be run in conjuction with the `nodiet` flag. Refer to `help` for more info on the requirements needed to run this. - ### `workshed` arg Garbo will determine when you are done with your current workshed, and automatically swap to this workshed mid-run. This argument is used in the following manner: e.g. `garbo workshed="cold medicine cabinet" ascend`. (It will also attempt simple string matching, so `workshed=cmc`, `workshed=pizza` or `workshed=trainset` should also work) diff --git a/mafia/data/garbo_settings.json b/mafia/data/garbo_settings.json index 1b7d850b9..d0ffa932f 100644 --- a/mafia/data/garbo_settings.json +++ b/mafia/data/garbo_settings.json @@ -31,11 +31,6 @@ "type": "boolean", "description": "Should a dinsey day pass be bought with FunFunds (if possible) at the end of the script?" }, - { - "name": "garbo_yachtzeechain", - "type": "boolean", - "description": "only diets after free fights, and attempts to estimate if Yachtzee! chaining is profitable for you. Requires Spring Break Beach access (it will not grab a one-day pass for you, but will make an attempt if one is used)." - }, { "name": "garbo_skipPillCheck", "type": "boolean", diff --git a/src/combat.ts b/src/combat.ts index 04bd43b89..1ddfc1006 100644 --- a/src/combat.ts +++ b/src/combat.ts @@ -453,7 +453,6 @@ export class Macro extends StrictMacro { kill(): Macro { const riftId = toInt($location`Shadow Rift`); - const doingYachtzee = globalOptions.prefs.yachtzeechain && !get("_garboYachtzeeChainCompleted"); const canPinata = haveEquipped($item`Cincho de Mayo`) && CinchoDeMayo.currentCinch() >= 5; return ( this.externalIf( @@ -461,7 +460,7 @@ export class Macro extends StrictMacro { Macro.trySkill($skill`Curse of Weaksauce`), ) .externalIf( - !doingYachtzee && canPinata, + canPinata, Macro.while_( `hasskill ${ $skill`Cincho: Projectile Piñata`.id diff --git a/src/config.ts b/src/config.ts index aabd767b2..985a354a5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -141,13 +141,6 @@ You can use multiple options in conjunction, e.g. "garbo nobarf ascend"', help: "Set to whatever you estimate the value of a free fight/run to be for you. (Default 2000)", default: 2000, }), - yachtzeechain: Args.flag({ - setting: "garbo_yachtzeechain", - help: "only diets after free fights, and attempts to estimate if Yachtzee! chaining is profitable for you - if so, it consumes a specific diet which uses ~0-36 spleen;\ - if not it automatically continues with the regular diet. Requires Spring Break Beach access (it will not grab a one-day pass for you, but will make an attempt if one is used).\ - Sweet Synthesis is strongly recommended, as with access to other meat% buffs from Source Terminal, Fortune Teller, KGB and the summoning chamber. Having access to a PYEC (on hand or in the clan stash) is a plus.", - default: false, - }), candydish: Args.flag({ setting: "garbo_candydish", help: "*DANGEROUS* garbo will consider using porcelain candy dishes. This could result in potentially destructive behavior in the instance that the user does not have sufficient meat (1-2 million) to purchase as many dishes as garbo desires or there is a price cliff.", diff --git a/src/extrovermectin.ts b/src/extrovermectin.ts index a12768565..013465313 100644 --- a/src/extrovermectin.ts +++ b/src/extrovermectin.ts @@ -39,7 +39,6 @@ import { freeFightFamiliar } from "./familiar"; import { freeRunConstraints, getUsingFreeBunnyBanish, ltbRun, setChoice } from "./lib"; import { garboAdventure, Macro } from "./combat"; import { acquire } from "./acquire"; -import { globalOptions } from "./config"; const embezzler = $monster`Knob Goblin Embezzler`; const crate = $monster`crate`; @@ -124,11 +123,7 @@ export function doingGregFight(): boolean { have($skill`Just the Facts`) && (get("_monsterHabitatsRecalled") < 3 || get("_monsterHabitatsFightsLeft") > 0); - return ( - extrovermectin || - habitat || - (globalOptions.prefs.yachtzeechain && !get("_garboYachtzeeChainCompleted")) - ); + return extrovermectin || habitat; } export function crateStrategy(): "Sniff" | "Saber" | "Orb" | null { diff --git a/src/fights.ts b/src/fights.ts index c71d34e0a..28684271e 100644 --- a/src/fights.ts +++ b/src/fights.ts @@ -1505,8 +1505,8 @@ const freeFightSources = [ return true; // Get the artifact or kill the boss immediately for free } - // Consider forcing noncombats below: - if (globalOptions.prefs.yachtzeechain) return false; // NCs are better when yachtzeeing, probably + if (realmAvailable("sleaze")) return false; + // TODO: With the KoL update, is there a function for checking if an NC is already forced? if (have($item`Clara's bell`) && !globalOptions.clarasBellClaimed) { return true; @@ -2159,9 +2159,6 @@ export function freeRunFights(): void { 1324: 5, // Fight a random partier }); - const onlyPriorityRuns = - globalOptions.prefs.yachtzeechain && !get("_garboYachtzeeChainCompleted", false); - const stashRun = stashAmount($item`navel ring of navel gazing`) ? $items`navel ring of navel gazing` : stashAmount($item`Greatest American Pants`) @@ -2173,7 +2170,6 @@ export function freeRunFights(): void { for (const priorityRunFight of priorityFreeRunFightSources) { priorityRunFight.runAll(); } - if (onlyPriorityRuns) return; for (const freeRunFightSource of freeRunFightSources) { freeRunFightSource.runAll(); } @@ -2740,7 +2736,6 @@ function runShadowRiftTurn(): void { // we can probably have a better name if (get("encountersUntilSRChoice") === 0) return; if ( - globalOptions.prefs.yachtzeechain || get("rufusQuestType") === "items" || get("rufusQuestType") === "entity" // We can't handle bosses... yet ) { diff --git a/src/index.ts b/src/index.ts index 783059264..f92f2b141 100644 --- a/src/index.ts +++ b/src/index.ts @@ -119,7 +119,6 @@ export function main(argString = ""): void { } Args.fill(globalOptions, argString); - globalOptions.prefs.yachtzeechain = false; if (globalOptions.version) return; // Since we always print the version, all done! if (globalOptions.help) { Args.showHelp(globalOptions); @@ -466,15 +465,7 @@ export function main(argString = ""): void { withStash(stashItems, () => { withVIPClan(() => { // 0. diet stuff. - if (globalOptions.nodiet || get("_garboYachtzeeChainCompleted", false)) { - print("We should not be yachtzee chaining", "red"); - globalOptions.prefs.yachtzeechain = false; - } - - if ( - !globalOptions.nodiet && - (!globalOptions.prefs.yachtzeechain || get("_garboYachtzeeChainCompleted", false)) - ) { + if (!globalOptions.nodiet) { runDiet(); } else if (!globalOptions.simdiet) { nonOrganAdventures(); @@ -498,8 +489,8 @@ export function main(argString = ""): void { // 2. do some embezzler stuff freeFights(); - yachtzeeChain(); dailyFights(); + yachtzeeChain(); if (!globalOptions.nobarf) { // 3. burn turns at barf diff --git a/src/outfit/dropsgearAccessories.ts b/src/outfit/dropsgearAccessories.ts index e6979d184..e7e0ddf4d 100644 --- a/src/outfit/dropsgearAccessories.ts +++ b/src/outfit/dropsgearAccessories.ts @@ -20,7 +20,6 @@ import { monsterManuelAvailable, } from "../lib"; import { garboValue } from "../garboValue"; -import { globalOptions } from "../config"; function mafiaThumbRing(mode: BonusEquipMode) { if (!have($item`mafia thumb ring`) || modeIsFree(mode)) { @@ -83,8 +82,6 @@ function cinchoDeMayo(mode: BonusEquipMode) { mode === BonusEquipMode.DMT || // Require manuel to make sure we don't kill during stasis !monsterManuelAvailable() || - // Don't use Cincho if we're planning on doing yachtzees, and haven't completed them yet - (!get("_garboYachtzeeChainCompleted") && globalOptions.prefs.yachtzeechain) || // If we have more than 50 passive damage, we'll never be able to cast projectile pinata without risking the monster dying maxPassiveDamage() >= 50 ) { diff --git a/src/post/index.ts b/src/post/index.ts index ef504b7ba..1f252f432 100644 --- a/src/post/index.ts +++ b/src/post/index.ts @@ -67,17 +67,12 @@ function floristFriars(): void { } function fillPantsgivingFullness(): void { - if ( - getRemainingStomach() > 0 && - (!globalOptions.prefs.yachtzeechain || get("_garboYachtzeeChainCompleted", false)) - ) { + if (getRemainingStomach() > 0) { consumeDiet(computeDiet().pantsgiving(), "PANTSGIVING"); } } function fillSweatyLiver(): void { - if (globalOptions.prefs.yachtzeechain && !get("_garboYachtzeeChainCompleted", false)) return; - const castsWanted = 3 - get("_sweatOutSomeBoozeUsed"); if (castsWanted <= 0 || !have($item`designer sweatpants`)) return; @@ -181,18 +176,16 @@ function funguySpores() { function refillCinch() { if (!CinchoDeMayo.have()) return; - if (get("_garboYachtzeeChainCompleted") || !globalOptions.prefs.yachtzeechain) { - const missingCinch = () => { - return 100 - CinchoDeMayo.currentCinch(); - }; - // Only rest if we'll get full value out of the cinch - // If our current cinch is less than the total available, it means we have free rests left. - while ( - missingCinch() > CinchoDeMayo.cinchRestoredBy() && - CinchoDeMayo.currentCinch() < CinchoDeMayo.totalAvailableCinch() - ) { - if (!freeRest()) break; - } + const missingCinch = () => { + return 100 - CinchoDeMayo.currentCinch(); + }; + // Only rest if we'll get full value out of the cinch + // If our current cinch is less than the total available, it means we have free rests left. + while ( + missingCinch() > CinchoDeMayo.cinchRestoredBy() && + CinchoDeMayo.currentCinch() < CinchoDeMayo.totalAvailableCinch() + ) { + if (!freeRest()) break; } } diff --git a/src/potions.ts b/src/potions.ts index 6e71600e4..7d2b65d0a 100644 --- a/src/potions.ts +++ b/src/potions.ts @@ -122,11 +122,7 @@ const availableItems = [ const validPawWishes: Map = new Map( wishableEffectData - .filter( - ({ e, name }) => - !invalidWishStrings.includes(name) && - (globalOptions.prefs.yachtzeechain ? e !== $effect`Eau d' Clochard` : true), // hardcoded heuristics - ) + .filter(({ name }) => !invalidWishStrings.includes(name)) .map(({ e, name, splitName }) => { if (!name.match(INVALID_CHARS_REGEX)) return [e, name]; diff --git a/src/turns.ts b/src/turns.ts index 5c0b5af5e..ed9753e07 100644 --- a/src/turns.ts +++ b/src/turns.ts @@ -34,19 +34,6 @@ export function estimatedGarboTurns(): number { : 0; const thumbRingMultiplier = usingThumbRing() ? 1 / 0.96 : 1; - // We need to estimate adventures from our organs if we are only dieting after yachtzee chaining - const yachtzeeTurns = 30; // guesstimate - const adventuresAfterChaining = - globalOptions.prefs.yachtzeechain && !get("_garboYachtzeeChainCompleted") - ? Math.max( - potentialFullnessAdventures() + - potentialInebrietyAdventures() + - potentialNonOrganAdventures() - - yachtzeeTurns, - 0, - ) - : 0; - let turns; if (globalOptions.stopTurncount) turns = globalOptions.stopTurncount - myTurncount(); else if (globalOptions.nobarf) turns = embezzlerCount(); @@ -55,8 +42,7 @@ export function estimatedGarboTurns(): number { (myAdventures() + sausageAdventures + pantsgivingAdventures + - thesisAdventures + - adventuresAfterChaining - + thesisAdventures - globalOptions.saveTurns) * thumbRingMultiplier; } else { @@ -65,8 +51,7 @@ export function estimatedGarboTurns(): number { sausageAdventures + pantsgivingAdventures + nightcapAdventures + - thesisAdventures + - adventuresAfterChaining) * + thesisAdventures) * thumbRingMultiplier; } diff --git a/src/yachtzee/buffs.ts b/src/yachtzee/buffs.ts deleted file mode 100644 index 8e18eb084..000000000 --- a/src/yachtzee/buffs.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { - cliExecute, - Effect, - haveEffect, - itemAmount, - maximize, - myMeat, - print, - setLocation, - toInt, - use, -} from "kolmafia"; -import { - $effect, - $item, - $items, - $location, - clamp, - get, - getActiveEffects, - getActiveSongs, - have, - isSong, - maxBy, - Mood, - set, -} from "libram"; -import { acquire } from "../acquire"; -import { withStash } from "../clan"; -import { baseMeat, burnLibrams, turnsToNC } from "../lib"; -import { - failedWishes, - farmingPotions, - mutuallyExclusive, - Potion, - variableMeatPotionsSetup, -} from "../potions"; -import { garboValue } from "../garboValue"; -import { executeNextDietStep } from "./diet"; -import { expectedEmbezzlers, pyecAvailable, shrugIrrelevantSongs } from "./lib"; - -export function yachtzeePotionProfits(potion: Potion, yachtzeeTurns: number): number { - // If we have an unused PYEC then - // 1) If we don't have an effect, +5 to gained effect duration - // 2) If we already have an effect, +5 to existing effect duration - // This means that the first use of a potion that we don't already have an effect of is more valuable than the next use - const PYECOffset = pyecAvailable() ? 5 : 0; - const existingOffset = haveEffect(potion.effect()) ? PYECOffset : 0; - const extraOffset = PYECOffset - existingOffset; - const effectiveYachtzeeTurns = Math.max( - Math.min( - yachtzeeTurns - haveEffect(potion.effect()) - existingOffset, - potion.effectDuration() + extraOffset, - ), - 0, - ); - const embezzlerTurns = Math.min( - expectedEmbezzlers, - Math.max(potion.effectDuration() + extraOffset - effectiveYachtzeeTurns, 0), - ); - const barfTurns = Math.max( - potion.effectDuration() + extraOffset - effectiveYachtzeeTurns - embezzlerTurns, - 0, - ); - const embezzlerValue = embezzlerTurns > 0 ? potion.gross(embezzlerTurns) : 0; - const yachtzeeValue = - (effectiveYachtzeeTurns * 2000 * (potion.meatDrop() + 2.5 * potion.familiarWeight())) / 100; // Every 1lbs of lep ~ 2.5% meat drop - const barfValue = (barfTurns * baseMeat * turnsToNC) / (turnsToNC + 1); - - return yachtzeeValue + embezzlerValue + barfValue - potion.price(true); -} - -const doublingValue = (potion: Potion, yachtzeeTurns: number) => - Math.min( - potion.price(false), - yachtzeePotionProfits(potion.doubleDuration(), yachtzeeTurns) - - Math.max(0, yachtzeePotionProfits(potion, yachtzeeTurns)), - ); - -export function yachtzeePotionSetup(yachtzeeTurns: number, simOnly?: boolean): number { - setLocation($location.none); - - let totalProfits = 0; - const PYECOffset = pyecAvailable() ? 5 : 0; - const excludedEffects = new Set(); - - shrugIrrelevantSongs(); - - if (have($item`Eight Days a Week Pill Keeper`) && !get("_freePillKeeperUsed")) { - const doublingPotions = farmingPotions.filter( - (potion) => - potion.canDouble && - haveEffect(potion.effect()) + PYECOffset * (have(potion.effect()) ? 1 : 0) < - yachtzeeTurns && - yachtzeePotionProfits(potion.doubleDuration(), yachtzeeTurns) > 0 && - potion.price(true) < myMeat(), - ); - const bestPotion = - doublingPotions.length > 0 - ? maxBy(doublingPotions, (potion) => doublingValue(potion, yachtzeeTurns)) - : undefined; - if (bestPotion) { - const profit = yachtzeePotionProfits(bestPotion.doubleDuration(), yachtzeeTurns); - const price = bestPotion.price(true); - totalProfits += profit; - print(`Determined that ${bestPotion.potion} was the best potion to double`, "blue"); - print( - `Expected to profit ${profit} meat from doubling 1 ${bestPotion.potion} @ price ${price} meat`, - "blue", - ); - if (!simOnly) { - cliExecute("pillkeeper extend"); - acquire(1, bestPotion.potion, profit + price); - bestPotion.use(1); - } else excludedEffects.add(bestPotion.effect()); - } - } - - for (const effect of getActiveEffects()) { - for (const excluded of mutuallyExclusive.get(effect) ?? []) { - excludedEffects.add(excluded); - } - } - - const testPotions = farmingPotions - .filter( - (potion) => - haveEffect(potion.effect()) + PYECOffset * toInt(haveEffect(potion.effect()) > 0) < - yachtzeeTurns && yachtzeePotionProfits(potion, yachtzeeTurns) > 0, - ) - .sort( - (a, b) => yachtzeePotionProfits(b, yachtzeeTurns) - yachtzeePotionProfits(a, yachtzeeTurns), - ); - - for (const potion of testPotions) { - const effect = potion.effect(); - const price = potion.price(true); - if ( - haveEffect(effect) + PYECOffset * toInt(haveEffect(effect) > 0) >= yachtzeeTurns || - price > myMeat() - ) { - continue; - } - if (!excludedEffects.has(effect)) { - let tries = 0; - while (haveEffect(effect) + PYECOffset * toInt(haveEffect(effect) > 0) < yachtzeeTurns) { - tries++; - print(`Considering effect ${effect} from source ${potion.potion}`, "blue"); - const profit = yachtzeePotionProfits(potion, yachtzeeTurns); - if (profit < 0) break; - const nPotions = have(effect) - ? clamp( - Math.floor( - (yachtzeeTurns - haveEffect(effect) - PYECOffset) / potion.effectDuration(), - ), - 1, - Math.max(1, yachtzeeTurns - PYECOffset), - ) - : 1; - - totalProfits += nPotions * profit; - print( - `Expected to profit ${nPotions * profit} meat from using ${nPotions} ${ - potion.potion - } @ price ${price} meat each`, - "blue", - ); - if (!simOnly) { - acquire(nPotions, potion.potion, profit + price, false); - if (itemAmount(potion.potion) < 1) break; - if (isSong(effect) && !have(effect)) { - for (const song of getActiveSongs()) { - const slot = Mood.defaultOptions.songSlots.find((slot) => slot.includes(song)); - if (!slot || slot.includes(effect)) { - cliExecute(`shrug ${song}`); - } - } - } - if ( - !potion.use(Math.min(itemAmount(potion.potion), nPotions)) || - tries >= 5 * Math.ceil(yachtzeeTurns / potion.effectDuration()) - ) { - break; - } else if ( - potion.potion === $item`pocket wish` && - failedWishes.includes(potion.effect()) - ) { - break; - } - } else break; - } - if (have(effect) || simOnly) { - for (const excluded of mutuallyExclusive.get(effect) ?? []) { - excludedEffects.add(excluded); - } - } - } - } - - if (!simOnly) { - variableMeatPotionsSetup(yachtzeeTurns, expectedEmbezzlers); - executeNextDietStep(true); - if (pyecAvailable()) { - maximize("MP", false); - if (have($item`Platinum Yendorian Express Card`)) { - burnLibrams(200); - use($item`Platinum Yendorian Express Card`); - } else { - withStash($items`Platinum Yendorian Express Card`, () => { - if (have($item`Platinum Yendorian Express Card`)) { - burnLibrams(200); - use($item`Platinum Yendorian Express Card`); - } - }); - } - } - if (have($item`License to Chill`) && !get("_licenseToChillUsed")) { - burnLibrams(200); - use($item`License to Chill`); - } - if (!get("_bagOTricksUsed")) { - withStash($items`Bag o' Tricks`, () => { - burnLibrams(200); - use($item`Bag o' Tricks`); - }); - } - burnLibrams(200); - set("_PYECAvailable", false); - } - - // Uncle Greenspan's may be cost effective - if (!simOnly && !have($effect`Buy! Sell! Buy! Sell!`)) { - const yachtzeeFactor = yachtzeeTurns * (yachtzeeTurns + 1); - const embezzlerFactor = - Math.min(100, expectedEmbezzlers + yachtzeeTurns) * - (Math.min(100, expectedEmbezzlers + yachtzeeTurns) + 1); - const greenspanValue = - (2000 * yachtzeeFactor + - (baseMeat + 750) * (embezzlerFactor - yachtzeeFactor) + - baseMeat * (10100 - embezzlerFactor)) / - 100; - const price = garboValue($item`Uncle Greenspan's Bathroom Finance Guide`); - const profit = greenspanValue - price; - if (profit > 0) { - print( - `Expected to profit ${profit} meat from using 1 Uncle Greenspan's Bathroom Finance Guide @ price ${price} meat`, - "blue", - ); - acquire(1, $item`Uncle Greenspan's Bathroom Finance Guide`, greenspanValue, false); - if (have($item`Uncle Greenspan's Bathroom Finance Guide`)) { - use(1, $item`Uncle Greenspan's Bathroom Finance Guide`); - } - } - } - return totalProfits; -} diff --git a/src/yachtzee/diet.ts b/src/yachtzee/diet.ts deleted file mode 100644 index 4ac43142f..000000000 --- a/src/yachtzee/diet.ts +++ /dev/null @@ -1,866 +0,0 @@ -import { - chew, - cliExecute, - drink, - eat, - equip, - fullnessLimit, - haveEffect, - inebrietyLimit, - itemAmount, - mallPrice, - myFullness, - myInebriety, - myLevel, - mySpleenUse, - numericModifier, - print, - retrievePrice, - spleenLimit, - toInt, - use, - useSkill, - visitUrl, -} from "kolmafia"; -import { - $effect, - $item, - $skill, - $slot, - CinchoDeMayo, - clamp, - get, - getAverageAdventures, - getSongCount, - getSongLimit, - have, - set, -} from "libram"; -import { acquire } from "../acquire"; -import { globalOptions } from "../config"; -import { hasMonsterReplacers } from "../extrovermectin"; -import { Potion } from "../potions"; -import { garboValue } from "../garboValue"; -import synthesize from "../synthesis"; -import { estimatedGarboTurns } from "../turns"; -import { yachtzeePotionProfits, yachtzeePotionSetup } from "./buffs"; -import { optimizeForFishy } from "./fishy"; -import { cinchNCs, freeNCs, pyecAvailable, shrugIrrelevantSongs, useSpikolodonSpikes } from "./lib"; -import { freeRest } from "../lib"; -import { shouldAugustCast } from "../resources"; - -class YachtzeeDietEntry { - name: string; - quantity: number; - fullness: number; - drunkenness: number; - spleen: number; - action: (n: number, name?: string) => T; - - constructor( - name: string, - quantity: number, - fullness: number, - drunkenness: number, - spleen: number, - action: (n: number, name?: string) => T, - ) { - this.name = name; - this.quantity = quantity; - this.fullness = fullness; - this.drunkenness = drunkenness; - this.spleen = spleen; - this.action = action; - } -} - -function ensureConsumable( - name: string, - n: number, - fullness: number, - inebriety: number, - spleenUse: number, -): void { - if (myFullness() + n * fullness > Math.max(fullnessLimit(), myFullness())) { - throw new Error(`Eating ${n} ${name} exceeds our stomach capacity!`); - } else if (myInebriety() + n * inebriety > inebrietyLimit()) { - throw new Error(`Drinking ${n} ${name} exceeds our liver capacity!`); - } else if (mySpleenUse() + n * spleenUse > spleenLimit()) { - throw new Error(`Using ${n} ${name} exceeds our spleen capacity!`); - } -} - -class YachtzeeDietUtils { - dietArray: Array>; - pref: string; - originalPref: string; - - constructor(action?: (n: number, name?: string) => void) { - this.originalPref = !get("_garboYachtzeeChainDiet") ? "" : get("_garboYachtzeeChainDiet"); - this.pref = ""; - this.dietArray = [ - new YachtzeeDietEntry("extra-greasy slider", 0, 5, 0, -5, (n: number) => { - ensureConsumable("extra-greasy slider", n, 5, 0, -5); - eat(n, $item`extra-greasy slider`); - }), - new YachtzeeDietEntry("jar of fermented pickle juice", 0, 0, 5, -5, (n: number) => { - ensureConsumable("jar of fermented pickle juice", n, 0, 5, -5); - castOde(5 * n); - drink(n, $item`jar of fermented pickle juice`); - }), - new YachtzeeDietEntry("Extrovermectin™", 0, 0, 0, 2, (n: number) => { - ensureConsumable("Extrovermectin™", n, 0, 0, 2); - chew(n, $item`Extrovermectin™`); - }), - new YachtzeeDietEntry("synthesis", 0, 0, 0, 1, (n: number) => { - ensureConsumable("synthesis", n, 0, 0, 1); - synthesize(n, $effect`Synthesis: Greed`); - }), - new YachtzeeDietEntry("mojo filter", 0, 0, 0, -1, (n: number) => { - use(n, $item`mojo filter`); - }), - new YachtzeeDietEntry("beggin' cologne", 0, 0, 0, 1, (n: number) => { - ensureConsumable("beggin' cologne", n, 0, 0, 1); - chew(n, $item`beggin' cologne`); - }), - new YachtzeeDietEntry("stench jelly", 0, 0, 0, 1, (n: number) => { - ensureConsumable("stench jelly", n, 0, 0, 1); - chew(n, $item`stench jelly`); - }), - new YachtzeeDietEntry("toast with stench jelly", 0, 1, 0, 0, (n: number) => { - ensureConsumable("toast with stench jelly", n, 1, 0, 0); - const VOA = get("valueOfAdventure"); - if (garboValue($item`munchies pill`) < 2.66 * VOA) { - acquire(n, $item`munchies pill`, 2.66 * VOA, false); // We should have already acquired this earlier (this is just a failsafe) - use(Math.min(n, itemAmount($item`munchies pill`)), $item`munchies pill`); - } - eat(n, $item`toast with stench jelly`); - }), - new YachtzeeDietEntry("jumping horseradish", 0, 1, 0, 0, (n: number) => { - ensureConsumable("jumping horseradish", n, 1, 0, 0); - eat(n, $item`jumping horseradish`); - }), - new YachtzeeDietEntry("Boris's bread", 0, 1, 0, 0, (n: number) => { - ensureConsumable("Boris's bread", n, 1, 0, 0); - eat(n, $item`Boris's bread`); - }), - new YachtzeeDietEntry("bottle of Greedy Dog", 0, 0, 3, 0, (n: number) => { - ensureConsumable("bottle of Greedy Dog", n, 0, 3, 0); - drink(n, $item`bottle of Greedy Dog`); - }), - new YachtzeeDietEntry("clara's bell", 0, 0, 0, 0, () => { - use($item`Clara's bell`); - globalOptions.clarasBellClaimed = true; - }), - new YachtzeeDietEntry("Deep Dish of Legend", 0, 2, 0, 0, (n: number) => { - ensureConsumable("Deep Dish of Legend", n, 2, 0, 0); - eat(n, $item`Deep Dish of Legend`); - }), - new YachtzeeDietEntry("jurassic parka", 0, 0, 0, 0, useSpikolodonSpikes), - new YachtzeeDietEntry("cinch fiesta", 0, 0, 0, 0, () => { - equip($slot`acc3`, $item`Cincho de Mayo`); - while (CinchoDeMayo.currentCinch() < 60) { - if (!freeRest()) throw new Error("We are out of free rests!"); - } - useSkill($skill`Cincho: Fiesta Exit`); - }), - ]; - if (action) this.dietArray.forEach((entry) => (entry.action = action)); - } - - public setDietEntry( - name: string, - qty?: number, - action?: (n: number, name?: string) => void, - ): void { - this.dietArray.forEach((entry) => { - if (entry.name === name) { - if (qty) entry.quantity = qty; - if (action) entry.action = action; - } - }); - } - - public resetDietPref(): void { - this.originalPref = ""; - this.pref = ""; - } - - public addToPref(n: number, name?: string): void { - if (!name) throw new Error("Diet pref must have a name"); - for (let i = 0; i < n; i++) { - this.pref = this.pref.concat(name ?? "").concat(","); - } - } - - public setDietPref(): void { - set("_garboYachtzeeChainDiet", this.originalPref.concat(this.pref)); - } -} - -function splitDietEntry(entry: YachtzeeDietEntry): Array> { - const entries = new Array>(); - for (let i = 0; i < entry.quantity; i++) { - entries.push( - new YachtzeeDietEntry( - entry.name, - 1, - entry.fullness, - entry.drunkenness, - entry.spleen, - entry.action, - ), - ); - } - return entries; -} - -function combineDietEntries( - left: YachtzeeDietEntry, - right: YachtzeeDietEntry, -): YachtzeeDietEntry { - return new YachtzeeDietEntry( - left.name, - left.quantity + right.quantity, - left.fullness, - left.drunkenness, - left.spleen, - left.action, - ); -} - -function castOde(turns: number): boolean { - if (!have($skill`The Ode to Booze`)) return false; - - shrugIrrelevantSongs(); - - // If we have the polka of plenty skill, we can re-buff up later - // Else, get rid of chorale which is the most inefficient song - if (getSongCount() - toInt(have($effect`Ode to Booze`)) >= getSongLimit()) { - if (have($skill`The Polka of Plenty`)) cliExecute(`shrug ${$effect`Polka of Plenty`}`); - else cliExecute(`shrug ${$effect`Chorale of Companionship`}`); - } - - while (haveEffect($effect`Ode to Booze`) < turns) { - useSkill($skill`The Ode to Booze`); - } - return true; -} - -export function executeNextDietStep(stopBeforeJellies?: boolean): void { - if (get("noncombatForcerActive")) return; - print("Executing next diet steps", "blue"); - const dietUtil = new YachtzeeDietUtils(); - dietUtil.resetDietPref(); - const VOA = get("valueOfAdventure"); - - const dietString = get("_garboYachtzeeChainDiet").split(","); - let stenchJellyConsumed = false; - for (const name of dietString) { - if (name.length === 0) continue; - else if ( - !stenchJellyConsumed && - (name.includes("stench jelly") || - ["clara's bell", "jurassic parka", "cinch fiesta"].includes(name)) - ) { - if (stopBeforeJellies) dietUtil.addToPref(1, name); - else { - const entry = dietUtil.dietArray.find((entry) => entry.name === name); - if (entry) { - if (entry.fullness > 0) { - if (shouldAugustCast($skill`Aug. 16th: Roller Coaster Day!`) && myFullness() > 0) { - useSkill($skill`Aug. 16th: Roller Coaster Day!`); - } - if (!get("_milkOfMagnesiumUsed")) { - acquire(1, $item`milk of magnesium`, 5 * VOA); - use(1, $item`milk of magnesium`); - } - if (!get("_distentionPillUsed") && have($item`distention pill`)) { - use(1, $item`distention pill`); - } - } - entry.action(1); - } else { - throw new Error(`Could not find ${name} in dietArray`); - } - } - stenchJellyConsumed = true; - } else if (!stenchJellyConsumed) { - dietUtil.dietArray.forEach((entry) => { - if (entry.name === name) { - if (entry.drunkenness > 0) { - while (get("sweat") >= 25 && get("_sweatOutSomeBoozeUsed") < 3 && myInebriety() > 0) { - useSkill($skill`Sweat Out Some Booze`); - } - if (!get("_syntheticDogHairPillUsed") && have($item`synthetic dog hair pill`)) { - use(1, $item`synthetic dog hair pill`); - } - } - if ( - myFullness() + entry.fullness > - Math.max(fullnessLimit(), myFullness()) + - (!get("_distentionPillUsed") && have($item`distention pill`) ? 1 : 0) - ) { - throw new Error(`consuming ${entry.name} will exceed our fullness limit`); - } else if (myInebriety() + entry.drunkenness > inebrietyLimit()) { - throw new Error(`consuming ${entry.name} will exceed our inebriety limit`); - } else if (mySpleenUse() + entry.spleen > spleenLimit()) { - throw new Error(`consuming ${entry.name} will exceed our spleen limit`); - } - if (entry.fullness > 0) { - if (!get("_milkOfMagnesiumUsed")) { - acquire(1, $item`milk of magnesium`, 5 * VOA); - use(1, $item`milk of magnesium`); - } - if (!get("_distentionPillUsed") && have($item`distention pill`)) { - use(1, $item`distention pill`); - } - } - entry.action(1); - } - }); - } else { - dietUtil.addToPref(1, name); - } - } - dietUtil.setDietPref(); - - if (!stenchJellyConsumed) { - throw new Error("We completed our entire diet but failed to get a stench jelly charge"); - } -} - -function yachtzeeDietScheduler( - menu: Array>, -): Array> { - const dietSchedule = new Array>(); - const remainingMenu = new Array>(); - const jellies = new Array>(); - const haveDistentionPill = !get("_distentionPillUsed") && have($item`distention pill`); - const toasts = new Array>(); - const freeNCs = new Array>(); - - // We assume the menu was constructed such that we will not overshoot our fullness and inebriety limits - // Assume all fullness/drunkenness > 0 non-spleen cleansers are inserted for buffs - // This makes it trivial to plan the diet - // First, lay out all the spleen cleansers (and the buff consumables at the front) - for (const entry of menu) { - if (entry.spleen < 0) { - for (const splitEntry of splitDietEntry(entry)) dietSchedule.push(splitEntry); - } else if (entry.name === "stench jelly") { - for (const splitEntry of splitDietEntry(entry)) jellies.push(splitEntry); - } else if (entry.name === "toast with stench jelly") { - for (const splitEntry of splitDietEntry(entry)) toasts.push(splitEntry); - } else if (entry.name === "jurassic parka") { - // Parka before Clara's, since we want to use free runs asap - // Note that since we push a flipped freeNCs onto the dietSchedule, so we put Clara's in front in freeNCs - for (const splitEntry of splitDietEntry(entry)) freeNCs.push(splitEntry); - } else if (entry.name === "clara's bell") { - for (const splitEntry of splitDietEntry(entry)) freeNCs.splice(0, 0, splitEntry); - } else if (entry.fullness > 0 || entry.drunkenness > 0) { - for (const splitEntry of splitDietEntry(entry)) dietSchedule.splice(0, 0, splitEntry); - } else { - for (const splitEntry of splitDietEntry(entry)) remainingMenu.push(splitEntry); - } - } - - // Place toasts at the back of our schedule - for (const toast of toasts) { - dietSchedule.push(toast); - } - - // Then, greedily inject spleen items into the schedule with the ordering: - // 1) Front to back of the schedule - // 2) Large spleen damagers to small spleen damagers - // This works because stench jellies are of size 1, so we can always pack efficiently using the greedy approach - remainingMenu.sort((left, right) => { - return right.spleen - left.spleen; - }); - - // Schedule jellies last so we definitely get spleen buffs first (e.g. synth and cologne) - for (const spleeners of [remainingMenu, jellies]) { - for (const entry of spleeners) { - let idx = 0; - let spleenUse = mySpleenUse(); - while ( - idx < dietSchedule.length && - ((dietSchedule[idx].spleen >= 0 && // We only insert if there's a cleanser immediately after where we want to insert - (entry.spleen >= 0 || spleenUse + dietSchedule[idx].spleen <= spleenLimit())) || - spleenUse + entry.spleen > spleenLimit() || // But don't insert if we will overshoot our spleen limit - (idx > 0 && - dietSchedule[idx - 1].spleen < 0 && - spleenUse + dietSchedule[idx].spleen >= 0)) // And cluster spleen cleansers (continue if the next cleanser can still clean our spleen) - ) { - spleenUse += dietSchedule[idx++].spleen ?? 0; - } - dietSchedule.splice(idx, 0, entry); - } - } - - // Place our free NC sources immediately before any jellies - for (let idx = 0; idx <= dietSchedule.length; idx++) { - if (idx === dietSchedule.length) { - for (const freeNCSource of freeNCs) dietSchedule.push(freeNCSource); - break; - } else if (dietSchedule[idx].name.includes("jelly")) { - for (const freeNCSource of freeNCs) dietSchedule.splice(idx, 0, freeNCSource); - break; - } - } - - // Next, combine clustered entries where possible (this is purely for aesthetic reasons) - let idx = 0; - while (idx < dietSchedule.length - 1) { - if (dietSchedule[idx].name === dietSchedule[idx + 1].name) { - dietSchedule.splice(idx, 2, combineDietEntries(dietSchedule[idx], dietSchedule[idx + 1])); - } else idx++; - } - - // Print diet schedule - print("Diet schedule:", "blue"); - for (const entry of dietSchedule) print(`Use ${entry.quantity} ${entry.name}`, "blue"); - - // Finally, run a check to ensure everything is fine - let fullness = myFullness(); - let drunkenness = myInebriety(); - let spleenUse = mySpleenUse(); - let sweatOutsAvailable = clamp( - Math.floor(get("sweat") / 25), - 0, - 3 - get("_sweatOutSomeBoozeUsed"), - ); - let syntheticPillsAvailable = - !get("_syntheticDogHairPillUsed") && have($item`synthetic dog hair pill`) ? 1 : 0; - for (const entry of dietSchedule) { - fullness += entry.quantity * entry.fullness; - for (let i = 0; i < entry.quantity; i++) { - while (drunkenness > 0 && sweatOutsAvailable > 0) { - drunkenness -= 1; - sweatOutsAvailable -= 1; - } - if (drunkenness > 0 && syntheticPillsAvailable > 0) { - drunkenness -= 1; - syntheticPillsAvailable -= 1; - } - drunkenness += entry.drunkenness; - } - spleenUse += entry.quantity * entry.spleen; - if (fullness > fullnessLimit() + toInt(haveDistentionPill) && entry.fullness > 0) { - throw new Error( - `Error in diet schedule: Overeating ${entry.quantity} ${entry.name} to ${fullness}/${ - fullnessLimit() + toInt(haveDistentionPill) - }`, - ); - } else if (drunkenness > inebrietyLimit() && entry.drunkenness > 0) { - throw new Error( - `Error in diet schedule: Overdrinking ${entry.quantity} ${ - entry.name - } to ${drunkenness}/${inebrietyLimit()}`, - ); - } else if (spleenUse > spleenLimit() && entry.spleen > 0) { - throw new Error( - `Error in diet schedule: Overspleening ${entry.quantity} ${ - entry.name - } to ${spleenUse}/${spleenLimit()}`, - ); - } - } - - print("Expected Organs Post-Yachtzee:"); - print( - `Fullness: ${myFullness()}/${fullnessLimit() + toInt(haveDistentionPill)} -> ${fullness}/${ - fullnessLimit() + toInt(haveDistentionPill) - }`, - "blue", - ); - print( - `Inebriety: ${myInebriety()}/${inebrietyLimit()} -> ${drunkenness}/${inebrietyLimit()}`, - "blue", - ); - print(`Spleen Use: ${mySpleenUse()}/${spleenLimit()} -> ${spleenUse}/${spleenLimit()}`, "blue"); - - return dietSchedule; -} - -export function yachtzeeChainDiet(simOnly?: boolean): boolean { - if (get("_garboYachtzeeChainDietPlanned", false)) return true; - set("_garboYachtzeeChainDiet", ""); - - const havePYECCharge = pyecAvailable(); - const haveDistentionPill = !get("_distentionPillUsed") && have($item`distention pill`); - visitUrl(`desc_item.php?whichitem=${$item`designer sweatpants`.descid}`); // Ensure that our sweat tracker is updated - const sweatOutsAvailable = clamp( - Math.floor(get("sweat") / 25), - 0, - 3 - get("_sweatOutSomeBoozeUsed"), - ); - const syntheticPillsAvailable = - !get("_syntheticDogHairPillUsed") && have($item`synthetic dog hair pill`) ? 1 : 0; - const lostStomachAvailable = shouldAugustCast($skill`Aug. 16th: Roller Coaster Day!`) ? 1 : 0; - - const currentSpleenLeft = spleenLimit() - mySpleenUse(); - let filters = 3 - get("currentMojoFilters"); - // save some spleen for the first three extros, which are worth a lot - // due to macrometeor and cheat code: replace enemy - const extroSpleenSpace = - hasMonsterReplacers() && !have($skill`Recall Facts: Monster Habitats`) - ? 6 - Math.min(6, 2 * get("beGregariousCharges")) - : 0; - const synthCastsToCoverRun = - globalOptions.nobarf || !have($skill`Sweet Synthesis`) - ? 0 - : Math.max( - 0, - Math.ceil((estimatedGarboTurns() - haveEffect($effect`Synthesis: Greed`)) / 30), - ); - const reservedFullness = - 2 * toInt(!get("deepDishOfLegendEaten")) + // to be consumed in yachtzee - 2 * toInt(!get("calzoneOfLegendEaten")) + // to be consumed post-yachtzee - 2 * toInt(!get("pizzaOfLegendEaten")) + // to be consumed post-yachtzee - 2; // subtract 2 for Boris Bread and Jumping Horseradish - const fullnessAvailable = - myLevel() >= 13 - ? Math.max( - 0, - fullnessLimit() - - myFullness() + - toInt(haveDistentionPill) + - lostStomachAvailable - - reservedFullness, - ) - : 0; - const reservedInebriety = Math.max( - 0, - itemAmount($item`astral pilsner`) - toInt(get("_mimeArmyShotglassUsed")), - ); - const inebrietyAvailable = - myLevel() >= 13 - ? Math.max( - 0, - inebrietyLimit() - - myInebriety() + - syntheticPillsAvailable + - sweatOutsAvailable - - reservedInebriety, - ) - : 0; - const spleenAvailable = currentSpleenLeft + filters; - const organsAvailable = - Math.floor(fullnessAvailable / 5) * 5 + // can only clean stomach in multiples of 5 - Math.floor(inebrietyAvailable / 5) * 5 + // can only clean liver in multiples of 5 - spleenAvailable; - - const cleanableSpleen = organsAvailable - synthCastsToCoverRun - extroSpleenSpace; - const sufficientOrgansFor = (yachtzees: number) => - cleanableSpleen >= yachtzees && // We can actually hit this many yachtzees - Math.floor((organsAvailable - extroSpleenSpace - yachtzees) / 5) * 5 >= synthCastsToCoverRun; // We must be able to cast enough turns of synth using cleansers - - const possibleJellyYachtzeeTurns = Array(15) - .fill(0) - .map((_, i) => 2 * (i + 1)) - .reverse(); - const jellyYachtzeeTurns = possibleJellyYachtzeeTurns.find(sufficientOrgansFor) ?? 0; - const canNCChain = freeNCs() > 0; - - if (jellyYachtzeeTurns === 0 && !canNCChain) { - print("Determined that there are no suitable number of turns to chain yachtzees", "red"); - return false; - } - - print(`Synth Casts Wanted: ${synthCastsToCoverRun}`, "blue"); - print(`Organs Available: ${organsAvailable}`, "blue"); - print(`Jelly Yachtzee Turns: ${jellyYachtzeeTurns}`, "blue"); - - // Plan our diet - - const sliders = Math.floor(fullnessAvailable / 5); - const pickleJuice = myLevel() >= 13 ? Math.floor(inebrietyAvailable / 5) : 0; - - const reqSynthTurns = 30; // We will be left with max(0, 30 - yachtzeeTurns) after chaining - const synthTurnsWanted = reqSynthTurns - haveEffect($effect`Synthesis: Greed`); - const synthCastsWanted = Math.ceil(synthTurnsWanted / 30); - const synthCasts = have($skill`Sweet Synthesis`) ? Math.max(synthCastsWanted, 0) : 0; - - let cologne = 0; - - const potentialSpleen = currentSpleenLeft + 5 * sliders + 5 * pickleJuice + filters; - const availableSpleen = potentialSpleen - synthCasts - extroSpleenSpace; // Spleen available for ingesting jellies - - set("_stenchJellyChargeTarget", 0); - - if (availableSpleen < jellyYachtzeeTurns) { - print("We were unable to generate enough organ space for optimal yachtzee chaining", "red"); - return false; - } - - let yachtzeeTurns = freeNCs() + jellyYachtzeeTurns; - if (availableSpleen + freeNCs() > yachtzeeTurns) cologne = 1; // If we have excess spleen, chew a cologne (representing -1 to availableSpleen, but we no longer need that variable) - - if (simOnly) print(`We can potentially run ${yachtzeeTurns} for yachtzee`, "purple"); - else print(`Trying to run ${yachtzeeTurns} turns of Yachtzee`, "purple"); - - // Compute prices to make sure everything is worth it - const fishyCost = optimizeForFishy(yachtzeeTurns); - const extroPrice = mallPrice($item`Extrovermectin™`); - const VOA = get("valueOfAdventure"); - const slidersPrice = mallPrice($item`extra-greasy slider`); - const pickleJuicePrice = mallPrice($item`jar of fermented pickle juice`); - const colognePrice = mallPrice($item`beggin' cologne`); - - // We prefer using pickle juice to cleanse our spleen for stench jellies since - // 1) It's cheaper - // 2) Our stomach can be used for horseradish buffs - const spleenNeeded = - Math.max(0, yachtzeeTurns - freeNCs()) + synthCasts + extroSpleenSpace + cologne; - const spleenToClean = spleenNeeded - currentSpleenLeft - filters; - - let pickleJuiceToDrink = clamp(Math.ceil(spleenToClean / 5), 0, pickleJuice); - let slidersToEat = clamp(Math.ceil(spleenToClean / 5) - pickleJuiceToDrink, 0, sliders); - let jelliesToChew = Math.max(0, yachtzeeTurns - freeNCs()); - - const synthToUse = synthCasts; - const cologneToChew = cologne; - - // Compare jellies + sliders vs toasts - const jellyPrice = mallPrice($item`stench jelly`); - const jellySlidersCosts = jellyPrice + slidersPrice / 5; - const jellyPickleCosts = jellyPrice + pickleJuicePrice / 5; - const toastPrice = Math.min( - mallPrice($item`toast with stench jelly`), - jellyPrice + mallPrice($item`toast`), - ); - - const sliderAdventuresPerFull = getAverageAdventures($item`extra-greasy slider`) / 5; - const toastAdventuresPerFull = getAverageAdventures($item`toast with stench jelly`) / 1; - const toastOpportunityCost = - toastPrice + (sliderAdventuresPerFull - toastAdventuresPerFull) * VOA; - - let toastsToEat = 0; - if (toastOpportunityCost < jellySlidersCosts || myLevel() < 13) { - toastsToEat = 5 * slidersToEat; - jelliesToChew -= 5 * slidersToEat; - slidersToEat = 0; - } - - if (toastOpportunityCost < jellyPickleCosts) { - while ( - pickleJuiceToDrink > 0 && - jelliesToChew >= 5 && - slidersToEat * 5 + toastsToEat + 5 <= fullnessAvailable - 1 - ) { - toastsToEat += 5; - jelliesToChew -= 5; - pickleJuiceToDrink -= 1; - } - } - - const jelliesBulkPrice = retrievePrice($item`stench jelly`, jelliesToChew); - - // TODO: This is outdated in the era of dynamic chains - if prices are too expensive, choose a more profitable chain length! - // If we need spleen cleansers but their prices are unreasonable, just return - const maxSliderPrice = 150000, - maxPickleJuicePrice = 150000; - if (slidersToEat > 0 && mallPrice($item`extra-greasy slider`) > maxSliderPrice) { - print("Sliders are way too overpriced for us to clean spleens for jellies", "red"); - return false; - } else if ( - pickleJuiceToDrink > 0 && - mallPrice($item`jar of fermented pickle juice`) > maxPickleJuicePrice - ) { - print("Pickle juices are way too overpriced for us to clean spleens for jellies", "red"); - return false; - } - - const horseradishes = - mallPrice($item`jumping horseradish`) <= 60000 && - haveEffect($effect`Kicked in the Sinuses`) < yachtzeeTurns && - 1 + slidersToEat * 5 + toastsToEat <= fullnessAvailable - ? 1 - : 0; - const borisBreads = - !get("unknownRecipe10978") && - retrievePrice($item`Boris's bread`) <= 60000 && - haveEffect($effect`Inspired Chef`) < yachtzeeTurns && - 1 + slidersToEat * 5 + toastsToEat + horseradishes <= fullnessAvailable - ? 1 - : 0; - const greedyDogs = - mallPrice($item`bottle of Greedy Dog`) <= 60000 && - haveEffect($effect`Covetin' Drunk`) < yachtzeeTurns && - 3 + pickleJuiceToDrink * 5 <= inebrietyAvailable - ? 1 - : 0; - // Opportunistically fit in Deep Dish of Legend only if we have enough stomach space - const pizzaAdditionalAdvPerFullness = 24 / 2 - 31.5 / 5; - const deepDishValue = - yachtzeePotionProfits(new Potion($item`Deep Dish of Legend`), yachtzeeTurns) + - pizzaAdditionalAdvPerFullness * 2 * VOA; - const deepDishPizzas = - !get("deepDishOfLegendEaten") && - deepDishValue > retrievePrice($item`Deep Dish of Legend`) && - !get("unknownRecipe11000") && - !get("unknownRecipe10988") && - !get("unknownRecipe10978") && - 2 + slidersToEat * 5 + toastsToEat + horseradishes + borisBreads <= fullnessAvailable - ? 1 - : 0; - - const earlyMeatDropsEstimate = - numericModifier("Meat Drop") + - (!have($effect`Synthesis: Greed`) && have($skill`Sweet Synthesis`) ? 300 : 0) + - (visitUrl("forestvillage.php").includes("friarcottage.gif") ? 60 : 0); - - // Some iffy calculations here - // If the best diet (at current prices) includes sliders and pickle juice (s+pj), no issues there - // However, if the best diet does not include s+pj, then we need to compute the loss of switching - // from the best diet to s+pj, and add it to our jellyValuePerSpleen calculations - // Let's just say (for now) that sliders are at best worth 70k and pickle juices are worth 60k - const slidersExcessCost = slidersPrice > 70000 ? slidersPrice - 70000 : 0; - const pickleJuiceExcessCost = pickleJuicePrice > 60000 ? pickleJuicePrice - 60000 : 0; - - // Yachtzee has higher base meat than KGEs - // thus some potions which aren't profitable for KGEs are profitable for yachtzees - // Prior to entering this function, we should already have triggered potionSetup() - // This means that any further buffs are purely profitable only for yachtzees - // If running simOnly, there's a possibility that potionSetup() hasn't been run - // However, this means that higherBaseMeatProfits would try to account for the lower earlyMeatDropsEstimate - const higherBaseMeatProfits = - yachtzeePotionSetup(yachtzeeTurns, true) + - cologneToChew * ((yachtzeeTurns + 60 + 5 * toInt(havePYECCharge)) * 1000 - colognePrice) + - (horseradishes > 0 ? yachtzeeTurns * 1000 : 0) + - (borisBreads > 0 ? yachtzeeTurns * 1000 : 0) + - (greedyDogs > 0 ? yachtzeeTurns * 2000 : 0); - - // We assume that the embezzlers after yachtzee chaining would still benefit from our start-of-day buffs - // so the assumption is that all the gregged embezzlies can be approximated as marginal KGEs with profits of 3 * VOA - const extroValuePerSpleen = 6 * VOA - extroPrice / 2; - const jellyValuePerSpleen = - (earlyMeatDropsEstimate * 2000) / 100 - - fishyCost / yachtzeeTurns - - (jelliesBulkPrice + - toastsToEat * toastPrice + - slidersToEat * slidersExcessCost + - pickleJuiceToDrink * pickleJuiceExcessCost - - higherBaseMeatProfits) / - jelliesToChew; - - print(`Early Meat Drop Modifier: ${earlyMeatDropsEstimate}%`); - print(`Extro value per spleen: ${extroValuePerSpleen}`); - print(`Jelly value per spleen: ${jellyValuePerSpleen}`); - if (simOnly) { - print( - `Jelly value estimates are wildly off for simulations because we have not properly buffed up yet`, - "orange", - ); - } - if (jellyValuePerSpleen < extroValuePerSpleen && !simOnly && jellyYachtzeeTurns > 0) { - // If we can't parka-chain, then return early - if (!canNCChain) { - print("Running extros is more profitable than chaining yachtzees", "red"); - return false; // We should do extros instead since they are more valuable - } - // Else, we do not want to use any toasts/jellies - yachtzeeTurns = freeNCs(); - slidersToEat = 0; - pickleJuiceToDrink = 0; - toastsToEat = 0; - jelliesToChew = 0; - filters = Math.min(mySpleenUse(), filters); // We may need to filter for synth/extros, but no longer need to filter for jellies - } - - // Schedule our diet first - - const addPref = (n: number, name?: string) => { - dietUtil.addToPref(n, name); - }; - const dietUtil = new YachtzeeDietUtils(addPref); - const regularEntries: [string, number][] = [ - ["extra-greasy slider", slidersToEat], - ["jar of fermented pickle juice", pickleJuiceToDrink], - ["synthesis", synthToUse], - ["mojo filter", filters], - ["beggin' cologne", cologneToChew], - ["jumping horseradish", horseradishes], - ["Boris's bread", borisBreads], - ["bottle of Greedy Dog", greedyDogs], - ["Deep Dish of Legend", deepDishPizzas], - ]; - - const specialEntries: [string, number, (n: number, name?: string) => void][] = ( - [ - ["stench jelly", jelliesToChew], - ["toast with stench jelly", toastsToEat], - ["clara's bell", have($item`Clara's bell`) && !globalOptions.clarasBellClaimed ? 1 : 0], - ["jurassic parka", have($item`Jurassic Parka`) ? 5 - get("_spikolodonSpikeUses") : 0], - ["cinch fiesta", cinchNCs()], - ] as [string, number][] - ).map(([name, qty]) => [ - name, - qty, - (n: number, name?: string) => { - dietUtil.addToPref(n, name); - if (!simOnly) { - set("_stenchJellyChargeTarget", get("_stenchJellyChargeTarget", 0) + n); - } - }, - ]); - - for (const entry of regularEntries) dietUtil.setDietEntry(...entry); - for (const entry of specialEntries) dietUtil.setDietEntry(...entry); - - // Run diet scheduler - print("Scheduling diet", "purple"); - const dietSchedule = yachtzeeDietScheduler(dietUtil.dietArray); - - // Now execute the diet - for (const entry of dietSchedule) entry.action(entry.quantity, entry.name); - dietUtil.setDietPref(); - - if (simOnly) return true; - - if (get("_stenchJellyChargeTarget", 0) < yachtzeeTurns) { - throw new Error( - `We are only able to obtain up to ${get( - "_stenchJellyChargeTarget", - 0, - )}/${yachtzeeTurns} turns of jelly charges!`, - ); - } - - // Acquire everything we need - acquire( - jelliesToChew, - $item`stench jelly`, - (2 * jelliesBulkPrice) / jelliesToChew, - true, - 1.2 * jelliesBulkPrice, // Bulk jelly purchases may cost > 1m in the future - ); - acquire( - toastsToEat, - $item`toast with stench jelly`, - 2 * toastPrice, - true, - 1.2 * toastPrice * toastsToEat, - ); - acquire(toastsToEat, $item`munchies pill`, 2.66 * VOA, false); - acquire(pickleJuiceToDrink, $item`jar of fermented pickle juice`, maxPickleJuicePrice); - acquire(slidersToEat, $item`extra-greasy slider`, maxSliderPrice); - acquire(cologneToChew, $item`beggin' cologne`, 2 * colognePrice); - acquire(filters, $item`mojo filter`, 2 * mallPrice($item`mojo filter`)); - acquire(horseradishes, $item`jumping horseradish`, 60000); - acquire(borisBreads, $item`Boris's bread`, 60000); - acquire(greedyDogs, $item`bottle of Greedy Dog`, 60000); - acquire(deepDishPizzas, $item`Deep Dish of Legend`, 1.2 * deepDishValue); - - // Get fishy turns - print("Getting fishy turns", "purple"); - optimizeForFishy(yachtzeeTurns, true); - - // Final checks - if (haveEffect($effect`Fishy`) + 5 * toInt(havePYECCharge) < yachtzeeTurns) { - throw new Error(`We only got ${haveEffect($effect`Fishy`)}/${yachtzeeTurns} turns of fishy!`); - } - - set("_garboYachtzeeChainDietPlanned", true); - return true; -} diff --git a/src/yachtzee/fishy.ts b/src/yachtzee/fishy.ts deleted file mode 100644 index c42f51d5f..000000000 --- a/src/yachtzee/fishy.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { - availableAmount, - booleanModifier, - canAdventure, - cliExecute, - equip, - haveEffect, - haveEquipped, - mallPrice, - print, - use, - useSkill, -} from "kolmafia"; -import { - $effect, - $familiar, - $item, - $location, - $skill, - get, - getActiveEffects, - have, - maxBy, - sum, - uneffect, -} from "libram"; -import { acquire } from "../acquire"; -import { garboAdventure, Macro } from "../combat"; -import { safeRestore } from "../lib"; -import { pyecAvailable, yachtzeeBuffValue } from "./lib"; -import { getBestWaterBreathingEquipment } from "./outfit"; -import { shouldAugustCast } from "../resources"; - -function fishyCloverAdventureOpportunityCost(pipe: boolean) { - const willBeFishy = pipe || have($effect`Fishy`); - const fishyCloverAdventureCost = willBeFishy ? 1 : 2; - const adventureExtensionBonus = pyecAvailable() ? 5 : 0; - return sum(getActiveEffects(), (currentBuff) => { - const buffValue = yachtzeeBuffValue(currentBuff); - if (buffValue <= 0) return 0; - - const currentBuffTurns = haveEffect(currentBuff); - if (currentBuffTurns <= fishyCloverAdventureCost) { - return (currentBuffTurns + adventureExtensionBonus) * buffValue; - } - return fishyCloverAdventureCost * buffValue; - }); -} - -export function optimizeForFishy(yachtzeeTurns: number, setup?: boolean): number { - // Returns the lowest cost for fishy - // Assume we already maximized for meat; this returns the cost of swapping out meat% equips for underwater breathing equips - const bestWaterBreathingEquipment = getBestWaterBreathingEquipment(yachtzeeTurns); - - if ( - setup && - !have($effect`Really Deep Breath`) && - bestWaterBreathingEquipment.item !== $item.none - ) { - equip(bestWaterBreathingEquipment.item); - } - if ( - haveEquipped($item`The Crown of Ed the Undying`) && - !booleanModifier("Adventure Underwater") - ) { - cliExecute("edpiece fish"); - } - // If we already have fishy, then we longer need to consider the cost of obtaining it - if (haveEffect($effect`Fishy`) >= yachtzeeTurns) return 0; - - // Restore here if we potentially need to visit an adventure.php zone to grab fishy turns - if (haveEffect($effect`Beaten Up`)) { - uneffect($effect`Beaten Up`); - } - safeRestore(); - - const haveFishyPipe = have($item`fishy pipe`) && !get("_fishyPipeUsed"); - const adventureExtensionBonus = pyecAvailable() ? 5 : 0; - const fishySources = [ - { - name: "fish juice box", - turns: 20 + (haveFishyPipe ? 10 : 0), - cost: mallPrice($item`fish juice box`), - action: () => { - acquire(1, $item`fish juice box`, 1.2 * mallPrice($item`fish juice box`)); - if (!have($item`fish juice box`)) throw new Error("Unable to obtain fish juice box"); - use(1, $item`fish juice box`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "2x fish juice box", - turns: 40 + (haveFishyPipe ? 10 : 0), - cost: 2 * mallPrice($item`fish juice box`), - action: () => { - acquire(2, $item`fish juice box`, 1.2 * mallPrice($item`fish juice box`)); - if (availableAmount($item`fish juice box`) < 2) { - throw new Error("Unable to obtain sufficient fish juice boxes"); - } - use(2, $item`fish juice box`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "cuppa Gill tea", - turns: 30 + (haveFishyPipe ? 10 : 0), - cost: mallPrice($item`cuppa Gill tea`) + bestWaterBreathingEquipment.cost, - action: () => { - acquire(1, $item`cuppa Gill tea`, 1.2 * mallPrice($item`cuppa Gill tea`)); - if (!have($item`cuppa Gill tea`)) throw new Error("Unable to obtain cuppa Gill tea"); - use(1, $item`cuppa Gill tea`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "powdered candy sushi set", - turns: 30 + (haveFishyPipe ? 10 : 0), - cost: mallPrice($item`powdered candy sushi set`) + bestWaterBreathingEquipment.cost, - action: () => { - acquire( - 1, - $item`powdered candy sushi set`, - 1.2 * mallPrice($item`powdered candy sushi set`), - ); - if (!have($item`powdered candy sushi set`)) { - throw new Error("Unable to obtain powdered candy sushi set"); - } - use(1, $item`powdered candy sushi set`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "concentrated fish broth", - turns: 30 + (haveFishyPipe ? 10 : 0), - cost: mallPrice($item`concentrated fish broth`) + bestWaterBreathingEquipment.cost, - action: () => { - acquire(1, $item`concentrated fish broth`, 1.2 * mallPrice($item`concentrated fish broth`)); - if (!have($item`concentrated fish broth`)) { - throw new Error("Unable to obtain concentrated fish broth"); - } - use(1, $item`concentrated fish broth`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "Lutz, the Ice Skate", - turns: 30 + (haveFishyPipe ? 10 : 0), - cost: - get("_skateBuff1") || get("skateParkStatus") !== "ice" - ? Infinity - : bestWaterBreathingEquipment.cost, - action: () => { - cliExecute("skate lutz"); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "Pocket Wish", - turns: 20 + (haveFishyPipe ? 10 : 0), - cost: mallPrice($item`pocket wish`) + bestWaterBreathingEquipment.cost, - action: () => { - acquire(1, $item`pocket wish`, 1.2 * mallPrice($item`pocket wish`)); - if (!have($item`pocket wish`)) { - throw new Error("Unable to obtain Pocket Wish"); - } - cliExecute(`genie effect Fishy`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "2x Pocket Wish", - turns: 40 + (haveFishyPipe ? 10 : 0), - cost: 2 * mallPrice($item`pocket wish`) + bestWaterBreathingEquipment.cost, - action: () => { - acquire(2, $item`pocket wish`, 1.2 * mallPrice($item`pocket wish`)); - if (availableAmount($item`pocket wish`) < 2) { - throw new Error("Unable to obtain Pocket Wish"); - } - cliExecute(`genie effect Fishy`); - cliExecute(`genie effect Fishy`); - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - }, - }, - { - name: "The Haggling (August Scepter)", - turns: 50 + (haveFishyPipe ? 10 : 0), - cost: canAdventure($location`The Brinier Deepers`) - ? (have($effect`Lucky!`) - ? 0 - : shouldAugustCast($skill`Aug. 2nd: Find an Eleven-Leaf Clover Day`) - ? 0 - : Infinity) + - get("valueOfAdventure") + - bestWaterBreathingEquipment.cost + - fishyCloverAdventureOpportunityCost(haveFishyPipe) - : Infinity, - action: () => { - if (!have($effect`Lucky!`)) { - if (have($familiar`Left-Hand Man`)) { - equip($familiar`Left-Hand Man`, $item.none); // Ensure that our scepter is not equipped on lefty - } - useSkill($skill`Aug. 2nd: Find an Eleven-Leaf Clover Day`); - if (!have($effect`Lucky!`)) { - throw new Error("Failed to acquire Lucky! from August Scepter"); - } - } - if (haveFishyPipe) use(1, $item`fishy pipe`); - garboAdventure($location`The Brinier Deepers`, Macro.abort()); - if (get("lastAdventure") !== "The Brinier Deepers") { - print( - "We failed to adventure in The Brinier Deepers, even though we thought we could. Try manually adventuring there for a lucky adventure.", - "red", - ); - } - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - if (haveEffect($effect`Fishy`) < yachtzeeTurns) { - throw new Error("Failed to get fishy from clover adv"); - } - }, - }, - { - name: "The Haggling (Clover)", - turns: 50 + (haveFishyPipe ? 10 : 0), - cost: canAdventure($location`The Brinier Deepers`) - ? (have($effect`Lucky!`) ? 0 : mallPrice($item`11-leaf clover`)) + - get("valueOfAdventure") + - bestWaterBreathingEquipment.cost + - fishyCloverAdventureOpportunityCost(haveFishyPipe) - : Infinity, - action: () => { - if (!have($effect`Lucky!`)) { - acquire(1, $item`11-leaf clover`, 1.2 * mallPrice($item`11-leaf clover`)); - if (!have($item`11-leaf clover`)) { - throw new Error("Unable to get 11-leaf clover for fishy!"); - } - use(1, $item`11-leaf clover`); - } - if (haveFishyPipe) use(1, $item`fishy pipe`); - garboAdventure($location`The Brinier Deepers`, Macro.abort()); - if (get("lastAdventure") !== "The Brinier Deepers") { - print( - "We failed to adventure in The Brinier Deepers, even though we thought we could. Try manually adventuring there for a lucky adventure.", - "red", - ); - } - if (haveFishyPipe && haveEffect($effect`Fishy`) + adventureExtensionBonus < yachtzeeTurns) { - use(1, $item`fishy pipe`); - } - if (haveEffect($effect`Fishy`) < yachtzeeTurns) { - throw new Error("Failed to get fishy from clover adv"); - } - }, - }, - { - name: "Just Fishy Pipe", - turns: 10, - cost: haveFishyPipe ? bestWaterBreathingEquipment.cost : Infinity, - action: () => { - if (haveFishyPipe && haveEffect($effect`Fishy`) < yachtzeeTurns) use(1, $item`fishy pipe`); - }, - }, - ]; - - const bestFishySource = maxBy( - fishySources.filter((source) => source.turns + haveEffect($effect`Fishy`) >= yachtzeeTurns), - "cost", - true, - ); - - print("Cost of viable Fishy sources:", "blue"); - fishySources - .filter((source) => source.turns + haveEffect($effect`Fishy`) >= yachtzeeTurns) - .forEach((source) => { - print(`${source.name} (${source.cost})`, "blue"); - }); - if (setup) { - print(`Taking best fishy source: ${bestFishySource.name}`, "blue"); - bestFishySource.action(); - } - return bestFishySource.cost; -} diff --git a/src/yachtzee/index.ts b/src/yachtzee/index.ts index 0cc5bfd0a..f03a59927 100644 --- a/src/yachtzee/index.ts +++ b/src/yachtzee/index.ts @@ -1,163 +1,116 @@ -import { - booleanModifier, - canInteract, - cliExecute, - equip, - haveEffect, - haveEquipped, - maximize, - myMeat, - myTurncount, - print, - useSkill, -} from "kolmafia"; +import { Engine, Task } from "grimoire-kolmafia"; +import { canAdventure, cliExecute, Item, use } from "kolmafia"; import { $effect, $item, + $items, $location, $skill, FloristFriar, get, - getActiveSongs, have, realmAvailable, - set, - uneffect, + Requirement, + tryFindFreeRun, } from "libram"; -import { garboAdventure, Macro } from "../combat"; -import { globalOptions } from "../config"; -import { postFreeFightDailySetup } from "../dailiespost"; -import { runDiet } from "../diet"; -import { embezzlerCount } from "../embezzler"; -import { doSausage, freeRunFights } from "../fights"; -import { baseMeat, eventLog, propertyManager, safeRestore } from "../lib"; -import { meatMood } from "../mood"; +import { freeRunConstraints, ltbRun, propertyManager } from "../lib"; +import { garboAdventureAuto, Macro } from "../combat"; +import { freeFightFamiliar } from "../familiar"; +import { wanderer } from "../garboWanderer"; +import { freeFightOutfit, toSpec } from "../outfit"; import postCombatActions from "../post"; -import { potionSetup } from "../potions"; -import { prepRobortender } from "../tasks/dailyFamiliars"; -import { yachtzeePotionSetup } from "./buffs"; -import { executeNextDietStep, yachtzeeChainDiet } from "./diet"; -import { pyecAvailable, shrugIrrelevantSongs } from "./lib"; -import { - getBestWaterBreathingEquipment, - maximizeMeat, - prepareOutfitAndFamiliar, - stickerSetup, -} from "./outfit"; +import { bestYachtzeeFamiliar } from "./familiar"; +import { waterBreathingEquipment } from "../outfit"; -function _yachtzeeChain(): void { - if (!canInteract()) return; - // We definitely need to be able to eat sliders and drink pickle juice - if (!realmAvailable("sleaze")) return; +function yachtzeeTasks(equipment: Item): Task[] { + const familiar = bestYachtzeeFamiliar(); - maximize("MP", false); - meatMood(false, 750 + baseMeat).execute(embezzlerCount()); - potionSetup(globalOptions.nobarf); // This is the default set up for embezzlers (which helps us estimate if chaining is better than extros) - maximizeMeat(); - prepareOutfitAndFamiliar(); + const meatOutfit = toSpec( + new Requirement( + [ + "meat", + ...(familiar.underwater || have($effect`Driving Waterproofly`) || have($effect`Wet Willied`) + ? [] + : ["underwater familiar"]), + ], + { + forceEquip: [equipment], + preventEquip: $items`anemoney clip, cursed magnifying glass, Kramco Sausage-o-Matic™, cheap sunglasses`, + modes: equipment === $item`The Crown of Ed the Undying` ? { edpiece: "fish" } : {}, + }, + ), + ); - const meatLimit = 5000000; - if (myMeat() > meatLimit) { - const meatToCloset = myMeat() - meatLimit; - print(""); - print(""); - print(`We are going to closet all-but-5million meat for your safety!`, "blue"); - print(""); - print(""); - if (!get("_yachtzeeChainClosetedMeat")) { - set("_yachtzeeChainClosetedMeat", meatToCloset); - } else { - set("_yachtzeeChainClosetedMeat", meatToCloset + get("_yachtzeeChainClosetedMeat")); - } - cliExecute(`closet put ${meatToCloset} meat`); - } - if (!yachtzeeChainDiet()) { - if (get("_yachtzeeChainClosetedMeat", 0)) { - cliExecute(`closet take ${get("_yachtzeeChainClosetedMeat")} meat`); - } - set("_yachtzeeChainClosetedMeat", 0); - return; - } - let jellyTurns = get("_stenchJellyChargeTarget", 0); - let fishyTurns = haveEffect($effect`Fishy`) + (pyecAvailable() ? 5 : 0); - let turncount = myTurncount(); - yachtzeePotionSetup(Math.min(jellyTurns, fishyTurns)); - stickerSetup(Math.min(jellyTurns, fishyTurns)); - if (get("_yachtzeeChainClosetedMeat", 0)) { - cliExecute(`closet take ${get("_yachtzeeChainClosetedMeat")} meat`); - } - set("_yachtzeeChainClosetedMeat", 0); - if (haveEffect($effect`Beaten Up`)) { - uneffect($effect`Beaten Up`); - } - meatMood(false, 2000).execute(Math.min(jellyTurns, fishyTurns)); - safeRestore(); + return [ + { + name: "Yachtzee", + ready: () => get("noncombatForcerActive") && have($effect`Fishy`), + completed: () => false, + do: $location`The Sunken Party Yacht`, + outfit: () => { + return { + familiar, - propertyManager.setChoice(918, 2); - let plantCrookweed = true; - while (Math.min(jellyTurns, fishyTurns) > 0) { - executeNextDietStep(); - if (!get("noncombatForcerActive")) throw new Error("We did not use stench jellies"); - // Switch familiars in case changes in fam weight from buffs means our current familiar is no longer optimal - prepareOutfitAndFamiliar(); - if (!have($effect`Really Deep Breath`)) { - const bestWaterBreathingEquipment = getBestWaterBreathingEquipment( - Math.min(jellyTurns, fishyTurns), - ); - if (bestWaterBreathingEquipment.item !== $item.none) { - equip(bestWaterBreathingEquipment.item); - } - if ( - haveEquipped($item`The Crown of Ed the Undying`) && - !booleanModifier("Adventure Underwater") - ) { - cliExecute("edpiece fish"); - } - } - if (!have($effect`Polka of Plenty`)) { - if (have($effect`Ode to Booze`)) cliExecute(`shrug ${$effect`Ode to Booze`}`); - if ( - getActiveSongs().length < (have($skill`Mariachi Memory`) ? 4 : 3) && - have($skill`The Polka of Plenty`) - ) { - useSkill($skill`The Polka of Plenty`); - } - } - garboAdventure($location`The Sunken Party Yacht`, Macro.abort()); - if (get("lastEncounter") === "Yachtzee!") eventLog.yachtzees += 1; - if (myTurncount() > turncount || haveEffect($effect`Fishy`) < fishyTurns) { - fishyTurns -= 1; - jellyTurns -= 1; - turncount = myTurncount(); - set("_stenchJellyChargeTarget", get("_stenchJellyChargeTarget", 0) - 1); - } - if (plantCrookweed && FloristFriar.have() && FloristFriar.Crookweed.available()) { - FloristFriar.Crookweed.plant(); - } - plantCrookweed = false; - postCombatActions(); + ...meatOutfit, + }; + }, + post: () => { + if ( + canAdventure($location`The Spooky Forest`) && + FloristFriar.have() && + FloristFriar.Crookweed.available() + ) { + FloristFriar.Crookweed.plant(); + } + }, + }, + { + name: "Fishy Pipe", + ready: () => have($item`fishy pipe`) && !have($effect`Fishy`), + completed: () => get("_fishyPipeUsed"), + do: () => use($item`fishy pipe`), + }, + { + name: "Spikolodon Spikes", + ready: () => have($item`Jurassic Parka`) && get("_spikolodonSpikeUses") < 5, + completed: () => get("noncombatForcerActive"), + do: () => { + const run = tryFindFreeRun(freeRunConstraints(false)) ?? ltbRun(); + const familiar = + run.constraints.familiar?.() ?? freeFightFamiliar({ allowAttackFamiliars: false }); + run.constraints.preparation?.(); + freeFightOutfit({ shirt: $item`Jurassic Parka`, ...toSpec(run), familiar }).dress(); + cliExecute("parka spikolodon"); - doSausage(); - } -} + const targetZone = $location`Sloppy Seconds Diner`; + const macro = Macro.familiarActions() + .skill($skill`Launch spikolodon spikes`) + .step(run.macro); -export function oldyachtzeeChain(): void { - if (!globalOptions.prefs.yachtzeechain) return; - if (get("_garboYachtzeeChainCompleted", false)) return; - print("Running Yachtzee Chain", "purple"); - _yachtzeeChain(); - set("_garboYachtzeeChainCompleted", true); - globalOptions.prefs.yachtzeechain = false; - if (!globalOptions.nodiet) { - shrugIrrelevantSongs(); - runDiet(); - prepRobortender(); // Recompute robo drinks' worth after diet is finally consumed - } - freeRunFights(); - postFreeFightDailySetup(); + const ncSkipper = wanderer().unsupportedChoices.get(targetZone); + if (ncSkipper) propertyManager.setChoices(ncSkipper); + garboAdventureAuto(targetZone, macro); + postCombatActions(); + }, + }, + { + name: "Clara's Bell", + ready: () => have($item`Clara's bell`) && !get("_claraBellUsed"), + completed: () => get("noncombatForcerActive"), + do: () => use($item`Clara's bell`), + }, + ]; } -export function yachtzeeChain(): void { - if (!globalOptions.prefs.yachtzeechain) return; - print("As of 2023-10-03, Yachtzee has been nerfed.", "red"); +export function yachtzeeChain() { + const equipment = waterBreathingEquipment.find((i) => have(i)); + + if (realmAvailable("sleaze") && equipment) { + const engine = new Engine(yachtzeeTasks(equipment)); + try { + engine.run(); + } finally { + engine.destruct(); + } + } } diff --git a/src/yachtzee/lib.ts b/src/yachtzee/lib.ts deleted file mode 100644 index bed5250a0..000000000 --- a/src/yachtzee/lib.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { cliExecute, Effect, Item } from "kolmafia"; -import { - $effect, - $familiar, - $item, - $items, - $location, - $skill, - CinchoDeMayo, - get, - getActiveSongs, - getModifier, - have, - Mood, - realmAvailable, - set, - sum, - tryFindFreeRun, -} from "libram"; -import { withStash } from "../clan"; -import { garboAdventureAuto, Macro } from "../combat"; -import { globalOptions } from "../config"; -import { embezzlerSources } from "../embezzler"; -import { freeFightFamiliar } from "../familiar"; -import { freeRunConstraints, ltbRun, propertyManager } from "../lib"; -import { freeFightOutfit, toSpec } from "../outfit"; -import postCombatActions from "../post"; -import { wanderer } from "../garboWanderer"; - -const ignoredSources = [ - "Orb Prediction", - "Pillkeeper Semirare", - "Lucky!", - "11-leaf clover (untapped potential)", -]; -export const expectedEmbezzlers = sum( - embezzlerSources.filter((source) => !ignoredSources.includes(source.name)), - (source) => source.potential(), -); - -export function pyecAvailable(): boolean { - if (get("_PYECAvailable") === "") { - set( - "_PYECAvailable", - get("expressCardUsed") - ? false - : have($item`Platinum Yendorian Express Card`) - ? true - : withStash($items`Platinum Yendorian Express Card`, () => { - return have($item`Platinum Yendorian Express Card`); - }), - ); - } - return get("_PYECAvailable", false); -} - -export function shrugIrrelevantSongs(): void { - for (const song of getActiveSongs()) { - const slot = Mood.defaultOptions.songSlots.find((slot) => slot.includes(song)); - if ( - !slot && - song !== $effect`Ode to Booze` && - song !== $effect`Polka of Plenty` && - song !== $effect`Chorale of Companionship` && - song !== $effect`The Ballad of Richie Thingfinder` - ) { - cliExecute(`shrug ${song}`); - } - } - // Shrug default Mood songs - cliExecute("shrug ur-kel"); - cliExecute("shrug phat loot"); -} - -export function cinchNCs(): number { - return CinchoDeMayo.have() ? Math.floor(CinchoDeMayo.totalAvailableCinch() / 60) : 0; -} - -export const freeNCs = (): number => - (have($item`Clara's bell`) && !globalOptions.clarasBellClaimed ? 1 : 0) + - (have($item`Jurassic Parka`) ? 5 - get("_spikolodonSpikeUses") : 0) + - cinchNCs(); - -export function yachtzeeBuffValue(obj: Item | Effect): number { - return (2000 * (getModifier("Meat Drop", obj) + getModifier("Familiar Weight", obj) * 2.5)) / 100; -} - -export function useSpikolodonSpikes(): void { - if (get("_spikolodonSpikeUses") >= 5) return; - const run = tryFindFreeRun(freeRunConstraints(false)) ?? ltbRun(); - - const canJelly = - have($familiar`Space Jellyfish`) && !run.constraints.familiar && realmAvailable("stench"); - const familiar = - run.constraints.familiar?.() ?? - (canJelly ? $familiar`Space Jellyfish` : freeFightFamiliar({ allowAttackFamiliars: false })); - run.constraints.preparation?.(); - freeFightOutfit({ shirt: $item`Jurassic Parka`, ...toSpec(run), familiar }).dress(); - cliExecute("parka spikolodon"); - - const targetZone = canJelly - ? $location`Pirates of the Garbage Barges` - : $location`Sloppy Seconds Diner`; - const macro = Macro.familiarActions() - .skill($skill`Launch spikolodon spikes`) - .step(run.macro); - const startingSpikes = get("_spikolodonSpikeUses"); - - const ncSkipper = wanderer().unsupportedChoices.get(targetZone); - if (ncSkipper) propertyManager.setChoices(ncSkipper); - - do { - garboAdventureAuto(targetZone, macro); - } while (get("_spikolodonSpikeUses") === startingSpikes); - - postCombatActions(); -} diff --git a/src/yachtzee/outfit.ts b/src/yachtzee/outfit.ts deleted file mode 100644 index a425977c4..000000000 --- a/src/yachtzee/outfit.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - canEquip, - equip, - equippedItem, - haveEquipped, - Item, - mallPrice, - myFamiliar, - toSlot, - use, - useFamiliar, -} from "kolmafia"; -import { - $effect, - $item, - $items, - $slot, - $slots, - findLeprechaunMultiplier, - get, - getModifier, - have, - maxBy, - Requirement, -} from "libram"; -import { acquire } from "../acquire"; -import { withStash } from "../clan"; -import { meatFamiliar } from "../familiar"; -import { baseMeat } from "../lib"; -import { familiarWaterBreathingEquipment, useUPCs, waterBreathingEquipment } from "../outfit"; -import { bestYachtzeeFamiliar } from "./familiar"; -import { expectedEmbezzlers, yachtzeeBuffValue } from "./lib"; - -export const maximizeMeat = (): boolean => - new Requirement( - [ - "meat", - ...(myFamiliar().underwater || - have($effect`Driving Waterproofly`) || - have($effect`Wet Willied`) - ? [] - : ["underwater familiar"]), - ], - { - preventEquip: $items`anemoney clip, cursed magnifying glass, Kramco Sausage-o-Matic™, cheap sunglasses`, - }, - ).maximize(); - -export function getBestWaterBreathingEquipment(yachtzeeTurns: number): { - item: Item; - cost: number; -} { - const waterBreathingEquipmentCosts = waterBreathingEquipment.map((it) => ({ - item: it, - cost: - have(it) && canEquip(it) - ? yachtzeeTurns * yachtzeeBuffValue(equippedItem(toSlot(it))) - : Infinity, - })); - const bestWaterBreathingEquipment = waterBreathingEquipment.some((item) => haveEquipped(item)) - ? { item: $item.none, cost: 0 } - : maxBy(waterBreathingEquipmentCosts, "cost", true); - return bestWaterBreathingEquipment; -} - -export function prepareOutfitAndFamiliar(): void { - useFamiliar(bestYachtzeeFamiliar()); - if ( - !get("_feastedFamiliars").includes(myFamiliar().toString()) && - get("_feastedFamiliars").split(";").length < 5 - ) { - withStash($items`moveable feast`, () => use($item`moveable feast`)); - } - maximizeMeat(); - if (!myFamiliar().underwater) { - equip( - $slot`familiar`, - maxBy( - familiarWaterBreathingEquipment.filter((it) => have(it)), - (eq) => getModifier("Familiar Weight", eq), - ), - ); - } -} - -export function stickerSetup(expectedYachts: number): void { - const currentStickers = $slots`sticker1, sticker2, sticker3`.map((s) => equippedItem(s)); - const UPC = $item`scratch 'n' sniff UPC sticker`; - if (currentStickers.every((sticker) => sticker === UPC)) return; - const yachtOpportunityCost = 25 * findLeprechaunMultiplier(bestYachtzeeFamiliar()); - const embezzlerOpportunityCost = 25 * findLeprechaunMultiplier(meatFamiliar()); - const addedValueOfFullSword = - ((75 - yachtOpportunityCost) * expectedYachts * 2000) / 100 + - ((75 - embezzlerOpportunityCost) * Math.min(20, expectedEmbezzlers) * (750 + baseMeat)) / 100; - if (mallPrice(UPC) < addedValueOfFullSword / 3) { - const needed = 3 - currentStickers.filter((sticker) => sticker === UPC).length; - if (needed) acquire(needed, UPC, addedValueOfFullSword / 3, false); - useUPCs(); - } -}