diff --git a/packages/garbo/src/resources/extrovermectin.ts b/packages/garbo/src/resources/extrovermectin.ts index 398ff5ecf..9b8b25933 100644 --- a/packages/garbo/src/resources/extrovermectin.ts +++ b/packages/garbo/src/resources/extrovermectin.ts @@ -44,6 +44,7 @@ import { freeFightFamiliar } from "../familiar"; import { freeRunConstraints, getUsingFreeBunnyBanish, + isFree, lastAdventureWasWeird, ltbRun, setChoice, @@ -63,6 +64,12 @@ type GregSource = { extra: number; }; +export const totalReplacers = () => + (have($skill`Meteor Lore`) ? 10 - get("_macrometeoriteUses") : 0) + + (have($item`Powerful Glove`) + ? Math.floor((100 - get("_powerfulGloveBatteryPowerUsed")) / 10) + : 0); + export function expectedGregs(skillSource: "habitat" | "extro"): number[] { const habitatCharges = have($skill`Just the Facts`) ? 3 - get("_monsterHabitatsRecalled", 0) @@ -95,20 +102,13 @@ export function expectedGregs(skillSource: "habitat" | "extro"): number[] { const orbGregs = have($item`miniature crystal ball`) ? 1 : 0; - const macrometeors = have($skill`Meteor Lore`) - ? 10 - get("_macrometeoriteUses") - : 0; - const replaceEnemies = have($item`Powerful Glove`) - ? Math.floor((100 - get("_powerfulGloveBatteryPowerUsed")) / 10) - : 0; - const firstReplaces = clamp( replacementsPerGreg(baseGregs[0]), 0, - macrometeors + replaceEnemies, + totalReplacers(), ); const initialCast: { replacesLeft: number; sources: GregSource[] } = { - replacesLeft: macrometeors + replaceEnemies - firstReplaces, + replacesLeft: totalReplacers() - firstReplaces, sources: [ { ...baseGregs[0], @@ -174,7 +174,9 @@ export function crateStrategy(): "Sniff" | "Saber" | "Orb" | null { ) { return "Sniff"; } - if (have($item`miniature crystal ball`)) return "Orb"; + if (have($item`miniature crystal ball`) && !isFree(globalOptions.target)) { + return "Orb"; + } if (have($item`Fourth of May Cosplay Saber`) && get("_saberForceUses") < 5) { return "Saber"; } diff --git a/packages/garbo/src/target/fights.ts b/packages/garbo/src/target/fights.ts index bbe2a5871..6928ecd13 100644 --- a/packages/garbo/src/target/fights.ts +++ b/packages/garbo/src/target/fights.ts @@ -764,7 +764,6 @@ export const conditionalSources = [ options.macro, ); changeLastAdvLocation(); - if (!doingGregFight()) set("_garbo_doneGregging", true); }, { spec: { equip: $items`miniature crystal ball` }, @@ -928,7 +927,7 @@ export const fakeSources = [ ), ]; -function copyTargetConfirmInvocation(msg: string): boolean { +export function copyTargetConfirmInvocation(msg: string): boolean { // If user does not have autoUserConfirm set to true // If the incocatedCount has already reached or exceeded the default limit if (!globalOptions.prefs.autoUserConfirm) { diff --git a/packages/garbo/src/target/lib.ts b/packages/garbo/src/target/lib.ts index fb74ed510..b67d35082 100644 --- a/packages/garbo/src/target/lib.ts +++ b/packages/garbo/src/target/lib.ts @@ -111,3 +111,41 @@ export function changeLastAdvLocationTask(): { }; } } + +export function puttyLeft(): number { + const havePutty = have($item`Spooky Putty sheet`); + const havePuttyMonster = have($item`Spooky Putty monster`); + const haveRainDoh = have($item`Rain-Doh black box`); + const haveRainDohMonster = have($item`Rain-Doh box full of monster`); + + const puttyUsed = get("spookyPuttyCopiesMade"); + const rainDohUsed = get("_raindohCopiesMade"); + const hardLimit = 6 - puttyUsed - rainDohUsed; + let monsterCount = 0; + let puttyLeft = 5 - puttyUsed; + let rainDohLeft = 5 - rainDohUsed; + + if (!havePutty && !havePuttyMonster) { + puttyLeft = 0; + } + if (!haveRainDoh && !haveRainDohMonster) { + rainDohLeft = 0; + } + + if (havePuttyMonster) { + if (get("spookyPuttyMonster") === globalOptions.target) { + monsterCount++; + } else { + puttyLeft = 0; + } + } + if (haveRainDohMonster) { + if (get("rainDohMonster") === globalOptions.target) { + monsterCount++; + } else { + rainDohLeft = 0; + } + } + const naiveLimit = Math.min(puttyLeft + rainDohLeft, hardLimit); + return naiveLimit + monsterCount; +} diff --git a/packages/garbo/src/tasks/copies/copies.ts b/packages/garbo/src/tasks/copies/copies.ts new file mode 100644 index 000000000..125a5bf63 --- /dev/null +++ b/packages/garbo/src/tasks/copies/copies.ts @@ -0,0 +1,408 @@ +import { + getClanLounge, + myHash, + myRain, + print, + runChoice, + runCombat, + toInt, + use, + useSkill, + visitUrl, +} from "kolmafia"; +import { + $effect, + $familiar, + $item, + $items, + $location, + $locations, + $skill, + ChateauMantegna, + ChestMimic, + CombatLoversLocket, + Counter, + get, + have, + HeavyRains, + property, + SourceTerminal, +} from "libram"; +import { acquire } from "../../acquire"; +import { globalOptions } from "../../config"; +import { + averageTargetNet, + HIGHLIGHT, + isFreeAndCopyable, + WISH_VALUE, +} from "../../lib"; +import { doingGregFight, gregReady } from "../../resources"; +import { copyTargetCount, copyTargetSources } from "../../target"; +import { puttyLeft } from "../../target/lib"; +import { CopyTargetTask } from "../engine"; +import { GarboStrategy, Macro } from "../../combat"; +import { wanderer } from "../../garboWanderer"; +import { copyTargetConfirmInvocation } from "../../target/fights"; + +let monsterInEggnet: boolean; +const mosterIsInEggnet = () => + (monsterInEggnet ??= ChestMimic.getReceivableMonsters().includes( + globalOptions.target, + )); + +// TO DO: non-trivial combat strategies, choosing outfit function based on copy target +// Outfit needs to support prof, angel, and underwater +// Combat can passively support prof probably +// Might be a weird edge case where we're copying a free fight and want to charge our professor +// But I think we're fine? + +export const CopyTargetFights: CopyTargetTask[] = ( + [ + { + name: "Digitize", + ready: () => + get("_sourceTerminalDigitizeMonster") === globalOptions.target && + Counter.get("Digitize Monster") <= 0, + completed: () => + Counter.get("Digitize Monster") > 0 || + !(get("_sourceTerminalDigitizeMonster") === globalOptions.target), + do: () => wanderer().getTarget("wanderer"), + fightType: "wanderer", + amount: () => + SourceTerminal.have() && SourceTerminal.getDigitizeUses() === 0 ? 1 : 0, + }, + { + name: "Guaranteed Romantic Monster", + ready: () => + get("_romanticFightsLeft") > 0 && + Counter.get("Romantic Monster window begin") <= 0 && + Counter.get("Romantic Monster window end") <= 0, + completed: () => get("_romanticFightsLeft") <= 0, + do: () => wanderer().getTarget("wanderer"), + fightType: "wanderer", + }, + { + name: "Enamorang", + ready: () => + Counter.get("Enamorang") <= 0 && + get("enamorangMonster") === globalOptions.target, + completed: () => Counter.get("Enamorang") <= 0, + do: () => wanderer().getTarget("wanderer"), + fightType: "wanderer", + }, + { + name: "Time-Spinner", + ready: () => + have($item`Time-Spinner`) && + $locations`Noob Cave, The Dire Warren, The Haunted Kitchen`.some( + (location) => + location.combatQueue.includes(globalOptions.target.name), + ) && + get("_timeSpinnerMinutesUsed") <= 7, + completed: () => get("_timeSpinnerMinutesUsed") > 7, + do: (): void => { + visitUrl(`inv_use.php?whichitem=${toInt($item`Time-Spinner`)}`); + runChoice(1); + visitUrl( + `choice.php?whichchoice=1196&monid=${globalOptions.target.id}&option=1`, + ); + }, + fightType: "regular", + amount: () => + have($item`Time-Spinner`) + ? Math.floor((10 - get("_timeSpinnerMinutesUsed")) / 3) + : 0, + }, + { + name: "Spooky Putty & Rain-Doh", + ready: () => + (have($item`Spooky Putty monster`) && + get("spookyPuttyMonster") === globalOptions.target) || + (have($item`Rain-Doh box full of monster`) && + get("rainDohMonster") === globalOptions.target), + completed: () => puttyLeft() < 1, + do: (): void => { + if ( + have($item`Spooky Putty monster`) && + get("spookyPuttyMonster") === globalOptions.target + ) { + use($item`Spooky Putty monster`); + } else if ( + have($item`Rain-Doh box full of monster`) && + get("rainDohMonster") === globalOptions.target + ) { + use($item`Rain-Doh box full of monster`); + } + }, + fightType: "regular", + amount: puttyLeft, + }, + { + name: "4-d Camera", + ready: () => + have($item`shaking 4-d camera`) && + get("cameraMonster") === globalOptions.target && + !get("_cameraUsed"), + completed: () => get("_cameraUsed"), + do: (): void => { + use($item`shaking 4-d camera`); + }, + fightType: "regular", + }, + { + name: "Ice Sculpture", + ready: () => + have($item`ice sculpture`) && + get("iceSculptureMonster") === globalOptions.target && + !get("_iceSculptureUsed"), + completed: () => get("_iceSculptureUsed"), + do: (): void => { + use($item`ice sculpture`); + }, + fightType: "regular", + }, + { + name: "Green Taffy", + ready: () => + have($item`envyfish egg`) && + get("envyfishMonster") === globalOptions.target && + !get("_envyfishEggUsed"), + completed: () => get("_envyfishEggUsed"), + do: (): void => { + use($item`envyfish egg`); + }, + fightType: "regular", + }, + { + name: "Screencapped Monster", + ready: () => + have($item`screencapped monster`) && + property.get("screencappedMonster") === globalOptions.target, + completed: () => + !have($item`screencapped monster`) || + !(property.get("screencappedMonster") === globalOptions.target), + do: (): void => { + use($item`screencapped monster`); + }, + fightType: "regular", + }, + { + name: "Sticky Clay Homunculus", + ready: () => + have($item`sticky clay homunculus`) && + property.get("crudeMonster") === globalOptions.target, + completed: () => + !have($item`sticky clay homunculus`) || + !(property.get("crudeMonster") === globalOptions.target), + do: (): void => { + use($item`sticky clay homunculus`); + }, + fightType: "regular", + }, + { + name: "Macrometeorite", + ready: () => + gregReady() && + have($skill`Meteor Lore`) && + get("_macrometeoriteUses") < 10, + completed: () => get("_macrometeoriteUses") >= 10, + do: $location`Noob Cave`, + canInitializeWandererCounters: true, + fightType: "gregarious", + amount: () => + doingGregFight() && have($skill`Meteor Lore`) + ? 10 - get("_macrometeoriteUses") + : 0, + }, + { + name: "Powerful Glove", + ready: () => + gregReady() && + have($item`Powerful Glove`) && + get("_powerfulGloveBatteryPowerUsed") <= 90, + completed: () => get("_powerfulGloveBatteryPowerUsed") >= 95, + outfit: { acc1: $item`Powerful Glove` }, + do: $location`The Dire Warren`, + canInitializeWandererCounters: true, + fightType: "gregarious", + amount: () => + doingGregFight() && have($item`Powerful Glove`) + ? Math.min((100 - get("_powerfulGloveBatteryPowerUsed")) / 10) + : 0, + }, + { + name: "Backup", + ready: () => + get("lastCopyableMonster") === globalOptions.target && + have($item`backup camera`) && + get("_backUpUses") < 11, + completed: () => get("_backUpUses") >= 11, + do: () => wanderer().getTarget("backup"), + canInitializeWandererCounters: true, + outfit: { + equip: $items`backup camera`, + modes: { backupcamera: "meat" }, + }, + pre: () => { + if ( + have($skill`Musk of the Moose`) && + !have($effect`Musk of the Moose`) + ) { + useSkill($skill`Musk of the Moose`); + } + }, + fightType: "backup", + amount: () => (have($item`backup camera`) ? 11 - get("_backUpUses") : 0), + }, + { + name: "Chateau Painting", + ready: () => + ChateauMantegna.have() && + !ChateauMantegna.paintingFought() && + ChateauMantegna.paintingMonster() === globalOptions.target, + completed: () => ChateauMantegna.paintingFought(), + do: (): void => { + ChateauMantegna.fightPainting(); + }, + fightType: "chainstarter", + }, + { + name: "Combat Lover's Locket", + completed: () => + !CombatLoversLocket.availableLocketMonsters().includes( + globalOptions.target, + ), + do: (): void => { + CombatLoversLocket.reminisce(globalOptions.target); + }, + fightType: "chainstarter", + }, + { + name: "Fax", + ready: () => + have($item`Clan VIP Lounge key`) && + !get("_photocopyUsed") && + have($item`photocopied monster`) && + property.get("photocopyMonster") === globalOptions.target && + getClanLounge()["deluxe fax machine"] !== undefined, + completed: () => get("_photocopyUsed"), + do: (): void => { + use($item`photocopied monster`); + }, + fightType: "chainstarter", + }, + { + name: "Mimic Egg", + completed: () => + ChestMimic.differentiableQuantity(globalOptions.target) < 1, + do: (): void => { + ChestMimic.differentiate(globalOptions.target); + }, + fightType: "chainstarter", + amount: () => ChestMimic.differentiableQuantity(globalOptions.target), + }, + { + name: "Rain Man", + ready: () => have($skill`Rain Man`) && myRain() >= 50, + completed: () => myRain() < 50, + do: (): void => { + HeavyRains.rainMan(globalOptions.target); + }, + fightType: "chainstarter", + amount: () => Math.floor(myRain() / 50), + }, + { + name: "Professor MeatChain", + completed: () => true, + do: (): void => {}, + fightType: "fake", + amount: () => + have($familiar`Pocket Professor`) && !get("_garbo_meatChain", false) + ? Math.max(10 - get("_pocketProfessorLectures"), 0) + : 0, + }, + { + name: "Professor WeightChain", + completed: () => true, + do: (): void => {}, + fightType: "fake", + amount: () => + have($familiar`Pocket Professor`) && !get("_garbo_weightChain", false) + ? Math.min(15 - get("_pocketProfessorLectures"), 5) + : 0, + }, + { + name: "Mimic Egg (from clinic)", + ready: () => + ChestMimic.have() && + $familiar`Chest Mimic`.experience >= 100 && + mosterIsInEggnet(), + completed: () => get("_mimicEggsObtained") >= 11, // gonna need help here, too I think. + do: (): void => { + ChestMimic.receive(globalOptions.target); + ChestMimic.differentiate(globalOptions.target); + }, + canInitializeWandererCounters: false, + fightType: "emergencychainstarter", + amount: () => 0, + }, + { + name: "Pocket Wish", + ready: () => + globalOptions.target.wishable && + get("_genieFightsUsed") >= 3 && + Math.floor(copyTargetCount()) > 1, + prepare: () => { + const potential = Math.floor(copyTargetCount()); + if (globalOptions.askedAboutWish) return globalOptions.wishAnswer; + const profit = (potential + 1) * averageTargetNet() - WISH_VALUE; + if (profit < 0) return false; + print( + `You have the following copy target sources untapped right now:`, + HIGHLIGHT, + ); + copyTargetSources + .filter((source) => source.potential() > 0) + .map((source) => `${source.potential()} from ${source.name}`) + .forEach((text) => print(text, HIGHLIGHT)); + globalOptions.askedAboutWish = true; + globalOptions.wishAnswer = copyTargetConfirmInvocation( + `Garbo has detected you have ${potential} potential ways to copy a ${ + globalOptions.target + }, but no way to start a fight with one. Current ${ + globalOptions.target + } net (before potions) is ${averageTargetNet()}, so we expect to earn ${profit} meat, after the cost of a wish. Should we wish for ${ + globalOptions.target + }?`, + ); + return globalOptions.wishAnswer; + }, + completed: () => + globalOptions.wishAnswer === false || get("_genieFightsUsed") >= 3, // gonna need help here, too I think. + do: (): void => { + acquire(1, $item`pocket wish`, WISH_VALUE); + visitUrl( + `inv_use.php?pwd=${myHash()}&which=3&whichitem=9537`, + false, + true, + ); + visitUrl( + `choice.php?pwd&whichchoice=1267&option=1&wish=to fight a ${globalOptions.target} `, + true, + true, + ); + visitUrl("main.php", false); + runCombat(); + globalOptions.askedAboutWish = false; + }, + canInitializeWandererCounters: true, + fightType: "emergencychainstarter", + amount: () => 0, + }, + ] as const +).map((partialTask) => ({ + combat: new GarboStrategy(() => Macro.target(partialTask.name)), + outfit: {}, + spendsTurn: () => !isFreeAndCopyable(globalOptions.target), + ...partialTask, +})); diff --git a/packages/garbo/src/tasks/copies/engine.ts b/packages/garbo/src/tasks/copies/engine.ts new file mode 100644 index 000000000..0a42aa17d --- /dev/null +++ b/packages/garbo/src/tasks/copies/engine.ts @@ -0,0 +1,252 @@ +import { DraggableFight } from "garbo-lib"; +import { Outfit } from "grimoire-kolmafia"; +import { Location, mallPrice, retrieveItem } from "kolmafia"; +import { + $familiar, + $item, + $items, + $location, + $monster, + $skill, + get, + have, + PocketProfessor, + Requirement, + set, + tryFindFreeRun, + undelay, +} from "libram"; +import { GarboStrategy, Macro } from "../../combat"; +import { globalOptions } from "../../config"; +import { freeFightFamiliar } from "../../familiar"; +import { + freeRunConstraints, + isFree, + ltbRun, + MEAT_TARGET_MULTIPLIER, + targettingMeat, +} from "../../lib"; +import { freeFightOutfit, meatTargetOutfit, toSpec } from "../../outfit"; +import { + crateStrategy, + doingGregFight, + hasMonsterReplacers, + totalGregCharges, +} from "../../resources"; +import { checkUnderwater } from "../../target/lib"; +import { BaseGarboEngine, CopyTargetTask } from "../engine"; + +export class CopyTargetEngine extends BaseGarboEngine { + private lastFight: CopyTargetTask | null = null; + private profChain: string | null = null; + private SPECIAL_TASKS = { + saberCrate: { + name: "Saber Crate", + completed: () => !get("_garbo_doneGregging", false) || !doingGregFight(), + ready: () => + (have($item`Fourth of May Cosplay Saber`) && + get("_saberForceUses") < 5 && + get("_saberForceMonsterCount") < 2) || + get("_saberForceMonster") !== $monster`crate`, + do: $location`Noob Cave`, + outfit: () => { + const run = + tryFindFreeRun( + freeRunConstraints({ equip: $items`Fourth of May Cosplay Saber` }), + ) ?? ltbRun(); + const spec = toSpec( + new Requirement([], { + forceEquip: $items`Fourth of May Cosplay Saber`, + preventEquip: $items`Kramco Sausage-o-Maticâ„¢, carnivorous potted plant`, + }).merge( + run.constraints.equipmentRequirements?.() ?? + new Requirement([], {}), + ), + ); + const familiar = + run.constraints.familiar?.() ?? + freeFightFamiliar({ canChooseMacro: false }); + return { + ...spec, + familiar, + }; + }, + choices: { + 1387: 2, + }, + combat: new GarboStrategy(() => { + const run = + tryFindFreeRun( + freeRunConstraints({ equip: $items`Fourth of May Cosplay Saber` }), + ) ?? ltbRun(); + return Macro.if_($monster`crate`, Macro.skill($skill`Use the Force`)) + .if_($monster`sausage goblin`, Macro.kill()) + .ifInnateWanderer(Macro.step(run.macro)) + .abort(); + }), + spendsTurn: false, + }, + } as const satisfies Record; + + draggable(task: CopyTargetTask): DraggableFight | null { + return ( + (["wanderer", "backup"] as const).find( + (fightType) => fightType === task.fightType, + ) ?? null + ); + } + + underwater(task: CopyTargetTask): boolean { + // Only run for copy target fights + if (!task.fightType) return false; + // Only run for _draggable_ copy target fights + if (!this.draggable(task)) return false; + // Only run if we can actually go underwater + if (!checkUnderwater()) return false; + // Only run if taffy is worth it + if ( + mallPrice($item`pulled green taffy`) > + (targettingMeat() + ? MEAT_TARGET_MULTIPLIER() * get("valueOfAdventure") + : get("valueOfAdventure")) && + retrieveItem($item`pulled green taffy`) + ) { + return false; + } + + return true; + } + + createOutfit(task: CopyTargetTask): Outfit { + if (task.fightType) { + const baseOutfit = undelay(task.outfit); + const spec = baseOutfit + ? baseOutfit instanceof Outfit + ? baseOutfit.spec() + : baseOutfit + : {}; + + // Prof chains + if (have($familiar`Pocket Professor`) && !spec.familiar) { + const chain = ["_garbo_meatChain", "_garbo_weightChain"].find( + (pref) => !get(pref, false), + ); + if (chain) { + this.profChain = chain; + spec.familiar = $familiar`Pocket Professor`; + spec.famequip ??= $item`Pocket Professor memory chip`; + spec.avoid ??= []; + spec.avoid.push($item`Roman Candelabra`); + if (chain === "_garbo_weightchain") { + return Outfit.from( + { ...spec, modifier: ["Familiar Weight"] }, + new Error("Unable to build outfit for weight chain!"), + ); + } + } + } + + if ( + !spec.familiar && + !get("_badlyRomanticArrows") && + !this.underwater(task) + ) { + const { familiar, famequip } = + [ + { familiar: $familiar`Reanimated Reanimator` }, + { + familiar: $familiar`Obtuse Angel`, + famequip: $item`quake of arrows`, + }, + ].find(({ familiar }) => have(familiar)) ?? {}; + if (familiar) { + spec.familiar = familiar; + if (famequip) spec.famequip ??= famequip; + } + } + + if (isFree(globalOptions.target)) { + const options = this.underwater(task) + ? { location: $location`The Briny Deeps` } + : {}; + return freeFightOutfit(spec, options); + } else { + if (task.do instanceof Location) return meatTargetOutfit(spec, task.do); + if (this.underwater(task)) { + return meatTargetOutfit(spec, $location`The Briny Deeps`); + } + return meatTargetOutfit(spec); + } + } + + return super.createOutfit(task); + } + + do(task: CopyTargetTask) { + if (this.profChain && PocketProfessor.currentlyAvailableLectures() <= 0) { + return; + } + + if (this.underwater(task)) { + return $location`The Briny Deeps`; + } + return super.do(task); + } + + // TODO: `proceedWithOrb` logic + // Reconsider the way it works for free fights? + // Reconsider + findAvailableFight(type: CopyTargetTask["fightType"]) { + return this.tasks.find( + (task) => task.fightType === type && this.available(task), + ); + } + + post(task: CopyTargetTask): void { + this.lastFight = task; + if (task.fightType === "gregarious" && totalGregCharges(true) === 0) { + set("_garbo_doneGregging", true); + } + + if (this.profChain) { + set(this.profChain, true); + this.profChain = null; + } + super.post(task); + } + + getNextTask(): CopyTargetTask | undefined { + // TO DO: allow for interpolating non-embezzler tasks into this + // E.g., kramco, digitize initialization, crate-sabers, and proton ghosts + // Actually I think those are it? I don't think there's a third + + // We do a wanderer if it's available, because they're basically involuntary + const wanderer = this.findAvailableFight("wanderer"); + if (wanderer) return wanderer; + + // Conditional fights we want to do when we can + // But we don't want to reset our orb with a gregarious fight; that defeats the purpose + const conditional = this.findAvailableFight("conditional"); + if (conditional?.fightType === "gregarious") { + if (this.available(this.SPECIAL_TASKS.saberCrate)) { + return this.SPECIAL_TASKS.saberCrate; + } + const hasReplacers = hasMonsterReplacers(); + + const skipConditionals = crateStrategy() === "Orb" && hasReplacers; + + if (!skipConditionals) return conditional; + } + + const regularCopy = + this.findAvailableFight("backup") ?? + this.findAvailableFight("regular") ?? + this.findAvailableFight("chainstarter"); + if (regularCopy) return regularCopy; + return ( + conditional ?? + this.findAvailableFight("emergencychainstarter") ?? + undefined + ); + } +} diff --git a/packages/garbo/src/tasks/engine.ts b/packages/garbo/src/tasks/engine.ts index 2fe6554e5..1cf66c961 100644 --- a/packages/garbo/src/tasks/engine.ts +++ b/packages/garbo/src/tasks/engine.ts @@ -30,6 +30,26 @@ export type GarboTask = StrictCombatTask & { duplicate?: Delayed; }; +export type CopyTargetTask = (GarboTask & { + canInitializeWandererCounters?: boolean; +}) & + ( + | { + fightType: + | "wanderer" + | "backup" + | "regular" + | "conditional" + | "chainstarter" + | "gregarious" + | "emergencychainstarter" + | "fake"; + wrongEncounterName?: boolean; + amount?: () => number; + } + | { fightType?: undefined } + ); + function logTargetFight(encounterType: string) { const isDigitize = encounterType.includes("Digitize Wanderer"); if (isDigitize) { @@ -43,8 +63,8 @@ function logTargetFight(encounterType: string) { /** A base engine for Garbo! * Runs extra logic before executing all tasks. */ -export class BaseGarboEngine extends Engine { - available(task: GarboTask): boolean { +export class BaseGarboEngine extends Engine { + available(task: T): boolean { safeInterrupt(); const taskSober = undelay(task.sobriety); if (taskSober) { @@ -57,7 +77,7 @@ export class BaseGarboEngine extends Engine { return super.available(task); } - dress(task: GarboTask, outfit: Outfit) { + dress(task: T, outfit: Outfit) { const duplicate = undelay(task.duplicate); if (duplicate && have($item`pro skateboard`) && !get("_epicMcTwistUsed")) { outfit.equip($item`pro skateboard`); @@ -68,12 +88,12 @@ export class BaseGarboEngine extends Engine { } } - prepare(task: GarboTask): void { + prepare(task: T): void { if ("combat" in task) safeRestore(); super.prepare(task); } - execute(task: GarboTask): void { + execute(task: T): void { const spentTurns = totalTurnsPlayed(); const duplicate = undelay(task.duplicate); const before = SourceTerminal.getSkills(); @@ -108,7 +128,7 @@ export class BaseGarboEngine extends Engine { * A safe engine for Garbo! * Treats soft limits as tasks that should be skipped, with a default max of one attempt for any task. */ -export class SafeGarboEngine extends BaseGarboEngine { +export class SafeGarboEngine extends BaseGarboEngine { constructor(tasks: GarboTask[]) { const options = new EngineOptions(); options.default_task_options = { limit: { skip: 1 } }; @@ -116,9 +136,9 @@ export class SafeGarboEngine extends BaseGarboEngine { } } -function runQuests( - quests: Quest[], - garboEngine: T, +function runQuests>( + quests: Quest[], + garboEngine: E, ) { const engine = new garboEngine(getTasks(quests));