Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cockroach copy target #2070

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
41fe572
skeleton structure
Shiverwarp Sep 6, 2024
933bda3
Merge branch 'main' of https://github.com/Loathing-Associates-Scripti…
Shiverwarp Oct 28, 2024
fb57591
defaultTarget
Shiverwarp Oct 28, 2024
1746ff6
Pirate Realm
Shiverwarp Oct 28, 2024
1096c26
Push some improvements and fixes to make this more accessible. (#2166)
Ignose Dec 2, 2024
11905e7
quick pass
spaghetti-squash Feb 7, 2025
acadb51
combine sgeeas with debuff potions
spaghetti-squash Feb 8, 2025
32ddb5b
format
spaghetti-squash Feb 8, 2025
6d930a9
Merge branch 'main' into cockroach
spaghetti-squash Feb 8, 2025
1a08983
fix piraterealm check
spaghetti-squash Feb 8, 2025
d10cafb
restructure: separate cockroach code across a few files
spaghetti-squash Feb 8, 2025
4b8e0a7
cache debuff items list
spaghetti-squash Feb 8, 2025
8ca75dd
de-invert `bestPotion` for legibility
spaghetti-squash Feb 8, 2025
b49ff0b
oh god I've gotten lost in the weeds a little
spaghetti-squash Feb 8, 2025
42b9625
I _think_ I've handled all the negatives correctly now
spaghetti-squash Feb 8, 2025
1e3395c
move `have` filter for caching
spaghetti-squash Feb 8, 2025
43e253f
Merge branch 'main' into cockroach
spaghetti-squash Feb 9, 2025
1c53b7c
shiver's suggestion for augmenting the check in `defaultTarget`
spaghetti-squash Feb 9, 2025
8493200
format & update import
spaghetti-squash Feb 9, 2025
6f1f46f
Decorative fountain
Shiverwarp Feb 10, 2025
7bcf729
Merge branch 'cockroach' of https://github.com/loathers/garbage-colle…
Shiverwarp Feb 10, 2025
a3d1b31
Decorative Fountain
Shiverwarp Feb 10, 2025
db169cd
Increase giant giant crab value
Shiverwarp Feb 10, 2025
63ffb4c
giant giant crab has 2000 base meat
Shiverwarp Feb 10, 2025
2ebe59b
Merge branch 'main' into cockroach
Ignose Feb 11, 2025
269e9be
Eyepatch caps at 20
Shiverwarp Feb 11, 2025
f534aeb
don't remove malignant effects
Shiverwarp Feb 11, 2025
29016db
don't use a free fight outfit when choosing an island; do when fighting
spaghetti-squash Feb 12, 2025
786f2ab
might as well fix this typo while i'm here
spaghetti-squash Feb 12, 2025
544a54c
only run roach stuff if we're doing greg fights
spaghetti-squash Feb 12, 2025
c79afb9
Update prep.ts
Ignose Feb 12, 2025
14a1a25
Safety in case we don't do prep
Ignose Feb 12, 2025
61a26f1
add check for `pirateRealmUnlockedAnemometer` in `defaultTarget`
spaghetti-squash Feb 13, 2025
5c380c1
lint for seraphiii
spaghetti-squash Feb 13, 2025
6d9c76e
modifiers in maximizer changes
Shiverwarp Feb 13, 2025
158a9b2
handle crab meat better
spaghetti-squash Feb 14, 2025
bae7461
tongue at end of roaches
spaghetti-squash Feb 14, 2025
688f5f6
else in meatTargetOutfit
Shiverwarp Feb 14, 2025
ec79afd
It's giant giant crab, not giant crab
Shiverwarp Feb 14, 2025
5e75b1e
handle beaten up better
spaghetti-squash Feb 14, 2025
faa96fc
Merge branch 'main' into cockroach
spaghetti-squash Feb 15, 2025
b9f1791
smoother check for whether gregs are on the table
spaghetti-squash Feb 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/garbo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
haveInCampground,
JuneCleaver,
maxBy,
realmAvailable,
set,
setCombatFlags,
setDefaultMaximizeOptions,
Expand Down Expand Up @@ -83,10 +84,13 @@ import { yachtzeeChain } from "./yachtzee";
import { garboAverageValue } from "./garboValue";
import {
BarfTurnQuests,
CockroachFinish,
CockroachSetup,
PostQuest,
runGarboQuests,
SetupTargetCopyQuest,
} from "./tasks";
import { doingGregFight, hasMonsterReplacers } from "./resources";

// Max price for tickets. You should rethink whether Barf is the best place if they're this expensive.
const TICKET_MAX_PRICE = 500000;
Expand All @@ -101,6 +105,14 @@ function ensureBarfAccess() {
}

function defaultTarget() {
// Can we account for re-entry if we only have certain amounts of copiers left in each of these?
// We need piraterealm if we're doing gregs of any sort; hasMonsterReplacers tells us if we're chewing extro
if (
!(doingGregFight() || hasMonsterReplacers()) ||
realmAvailable("pirate")
) {
return $monster`cockroach`;
}
if ($skills`Curse of Weaksauce, Saucegeyser`.every((s) => have(s))) {
return maxBy(
$monsters.all().filter((m) => m.wishable && isFreeAndCopyable(m)),
Expand Down Expand Up @@ -523,6 +535,11 @@ export function main(argString = ""): void {
// FIXME: Dynamically figure out pointer ring approach.
withStash(stashItems, () => {
withVIPClan(() => {
// Prepare pirate realm if our copy target is cockroach
// How do we handle if garbo was started without enough turns left without dieting to prep?
if (globalOptions.target === $monster`cockroach`) {
runGarboQuests([CockroachSetup]);
}
// 0. diet stuff.
if (
globalOptions.nodiet ||
Expand Down Expand Up @@ -560,7 +577,7 @@ export function main(argString = ""): void {

// 2. do some target copy stuff
freeFights();
runGarboQuests([SetupTargetCopyQuest]);
runGarboQuests([CockroachFinish, SetupTargetCopyQuest]);
yachtzeeChain();
dailyFights();

Expand Down
7 changes: 7 additions & 0 deletions packages/garbo/src/potions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ export interface PotionOptions {
}>;
}

export const VALUABLE_MODIFIERS = [
"Meat Drop",
"Familiar Weight",
"Smithsness",
"Item Drop",
] as const;

export class Potion {
potion: Item;
providesDoubleDuration?: boolean;
Expand Down
6 changes: 1 addition & 5 deletions packages/garbo/src/resources/extrovermectin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,7 @@ export function doingGregFight(): boolean {
(get("_monsterHabitatsRecalled") < 3 ||
get("_monsterHabitatsFightsLeft") > 0);

return (
extrovermectin ||
habitat ||
(globalOptions.prefs.yachtzeechain && !get("_garboYachtzeeChainCompleted"))
);
return extrovermectin || habitat;
}

const isOlfacted = (monster: Monster): boolean =>
Expand Down
60 changes: 60 additions & 0 deletions packages/garbo/src/tasks/cockroach/finish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Quest } from "grimoire-kolmafia";
import { $items, $location, get, questStep } from "libram";
import { GarboStrategy, Macro } from "../../combat";
import { targetMeat } from "../../lib";
import { meatMood } from "../../mood";
import { meatTargetOutfit } from "../../outfit";
import { potionSetup } from "../../potions";
import { copyTargetCount } from "../../target";
import { GarboTask } from "../engine";
import { checkAndFixOvercapStats } from "./lib";

export const CockroachFinish: Quest<GarboTask> = {
name: "Setup Cockroach Target",
ready: () => get("pirateRealmUnlockedAnemometer"),
completed: () => get("_lastPirateRealmIsland") === $location`Trash Island`,
tasks: [
{
name: "Final Island Encounter (Island 1 (Giant Giant Crab))",
ready: () =>
questStep("_questPirateRealm") === 5 &&
get("_lastPirateRealmIsland") === $location`Crab Island`,
completed: () => questStep("_questPirateRealm") > 5,
prepare: () => {
meatMood(true, targetMeat()).execute(copyTargetCount());
potionSetup(false);
checkAndFixOvercapStats();
},
do: $location`Crab Island`,
outfit: () => {
const spec = meatTargetOutfit({
modifier: ["meat"],
equip: $items`PirateRealm eyepatch`,
avoid: $items`Roman Candelabra`,
});
return spec;
},
choices: { 1385: 1, 1368: 1 }, // Take cocoa of youth, fight crab
combat: new GarboStrategy(() => Macro.delevel().meatKill()),
limit: { tries: 1 },
spendsTurn: true,
},
{
name: "Choose Trash Island",
ready: () => questStep("_questPirateRealm") === 6,
completed: () => questStep("_questPirateRealm") > 6,
prepare: () => checkAndFixOvercapStats(),
do: $location`Sailing the PirateRealm Seas`,
outfit: {
equip: $items`PirateRealm eyepatch`,
avoid: $items`Roman Candelabra`,
},
choices: { 1353: 5 }, // Trash Island
limit: { tries: 1 },
spendsTurn: false,
combat: new GarboStrategy(() =>
Macro.abortWithMsg("Hit a combat while sailing the high seas!"),
),
},
],
};
2 changes: 2 additions & 0 deletions packages/garbo/src/tasks/cockroach/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { CockroachSetup } from "./prep";
export { CockroachFinish } from "./finish";
227 changes: 227 additions & 0 deletions packages/garbo/src/tasks/cockroach/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import {
abort,
Effect,
effectModifier,
isShruggable,
Item,
mallPrice,
myBuffedstat,
retrieveItem,
Stat,
StatType,
use,
} from "kolmafia";
import {
$effect,
$item,
$items,
clamp,
get,
getActiveEffects,
getModifier,
have,
maxBy,
uneffect,
} from "libram";
import { VALUABLE_MODIFIERS } from "../../potions";
import { garboValue } from "../../garboValue";

function asEffect(thing: Item | Effect): Effect {
return thing instanceof Effect ? thing : effectModifier(thing, "Effect");
}

function improvesStat(thing: Item | Effect, stat: Stat): boolean {
const effect = asEffect(thing);
return ([stat.toString(), `${stat.toString()} Percent`] as const).some(
(modifier) => getModifier(modifier, effect) > 0,
);
}

function improvedStats(thing: Item | Effect): Stat[] {
return Stat.all().filter((stat) => improvesStat(thing, stat));
}
function improvesAStat(thing: Item | Effect): boolean {
return improvedStats(thing).length > 0;
}

function isValuable(thing: Item | Effect): boolean {
const effect = asEffect(thing);
return VALUABLE_MODIFIERS.some(
(modifier) => getModifier(modifier, effect) > 0,
);
}

const debuffedEnough = () =>
Stat.all().every((stat) => myBuffedstat(stat) <= 100);

const effectiveDebuffQuantity = (
effect: Effect,
stat: Stat,
shrugging: boolean,
) =>
clamp(
(shrugging ? -1 : 1) *
(getModifier(stat.toString(), effect) +
// Eyepatch caps you at 30
(30 / 100) * getModifier(`${stat.toString()} Percent`, effect)),
100 - myBuffedstat(stat),
0,
);

function debuffEfficacy(
item: Item,
effect: Effect,
stat: Stat,
shrugging: boolean,
) {
return (
(-1 * effectiveDebuffQuantity(effect, stat, shrugging)) / mallPrice(item)
);
}

const itemBanList = $items`pill cup`;

const possibleDebuffItems: Partial<
Record<StatType, { item: Item; effect: Effect }[]>
> = {};
const getDebuffItems = (stat: Stat) =>
(possibleDebuffItems[stat.toString()] ??= Item.all()
.filter(
(i) =>
i.potion &&
(i.tradeable || have(i)) &&
!itemBanList.includes(i) &&
!improvesAStat(i),
)
.map((item) => ({ item, effect: asEffect(item) }))
.filter(
({ effect }) =>
effect !== $effect.none &&
!have(effect) &&
([stat.toString(), `${stat.toString()} Percent`] as const).some(
(mod) => getModifier(mod, effect) < 0,
),
)).filter(({ effect }) => !have(effect));

function getBestDebuffItem(stat: Stat): Item | Effect {
const bestPotion = maxBy(getDebuffItems(stat), ({ item, effect }) =>
debuffEfficacy(item, effect, stat, false),
);

const effectsToShrug = getActiveEffects().filter(
(ef) => !isShruggable(ef) && shouldRemove(ef),
);

if (!effectsToShrug.length) return bestPotion.item;

const bestEffectToShrug = maxBy(
effectsToShrug,
(ef) => effectiveDebuffQuantity(ef, stat, true),
true,
);
return effectiveDebuffQuantity(bestEffectToShrug, stat, true) /
mallPrice($item`soft green echo eyedrop antidote`) >
effectiveDebuffQuantity(bestPotion.effect, stat, false) /
mallPrice(bestPotion.item)
? bestEffectToShrug
: bestPotion.item;
}

function shouldRemove(effect: Effect) {
// Only shrug effects that buff at least one stat that's too high
if (!improvedStats(effect).some((stat) => myBuffedstat(stat) >= 100)) {
return false;
}
// Never shrug effects that give meat or whatever
if (isValuable(effect)) return false;
return true;
}

// Just checking for the gummi effects for now, maybe can check other stuff later?
export function checkAndFixOvercapStats(): void {
if (debuffedEnough()) return;

for (const effect of getActiveEffects()) {
if (!isShruggable(effect)) continue;
if (!shouldRemove(effect)) continue;

uneffect(effect);

if (debuffedEnough()) return;
}

let debuffItemLoops = 0;
while (!debuffedEnough()) {
if (debuffItemLoops > 27) {
abort("Spent too long trying to debuff for PirateRealm!");
}

debuffItemLoops++;
for (const stat of Stat.all()) {
if (myBuffedstat(stat) > 100) {
const debuff = getBestDebuffItem(stat);
if (debuff instanceof Item) {
retrieveItem(debuff);
use(debuff);
} else {
uneffect(debuff);
}
}
}
}

if (!debuffedEnough()) {
abort(
"Buffed stats are too high for PirateRealm! Check for equipment or buffs that we can add to prevent in the script",
);
}
}

export function dessertIslandWorthIt(): boolean {
// estimating value of giant crab at 3*VOA
return garboValue($item`cocoa of youth`) > 3 * get("valueOfAdventure");
}

function crewRoleValue(crewmate: string): number {
// Cuisinier is highest value if cocoa of youth is more meat than expected from giant crab
if (dessertIslandWorthIt() && crewmate.includes("Cuisinier")) {
return 50;
}
// Coxswain helps save turns if we run from storms
if (crewmate.includes("Coxswain")) return 40;
// Harquebusier gives us extra fun from combats
if (crewmate.includes("Harquebusier")) return 30;
// Crypto, Cuisinier (if cocoa not worth it), and Mixologist have small bonuses we care about less
return 0;
}

function crewAdjectiveValue(crewmate: string): number {
// Wide-Eyed give us bonus fun when counting birds in smooth sailing, and we'll mostly be doing that rather than spending limited grub/grog
if (crewmate.includes("Wide-Eyed")) return 5;
// Gluttonous can help when running out of grub, even though we usually shouldn't?
if (crewmate.includes("Gluttonous")) return 4;
// Beligerent, Dipsomaniacal, and Pinch-Fisted don't make much difference
return 0;
}

export function bestCrewmate(): 1 | 2 | 3 {
return maxBy([1, 2, 3], (choiceOption) => {
const crewmatePref = `_pirateRealmCrewmate${choiceOption}`;
const crewmate = get(crewmatePref);
const roleValue = crewRoleValue(crewmate);
const adjectiveValue = crewAdjectiveValue(crewmate);
return roleValue + adjectiveValue;
});
}

export function outfitBonuses() {
const funPointValue = garboValue($item`PirateRealm guest pass`) / 600;
return new Map([
[
$item`carnivorous potted plant`,
get("valueOfAdventure") / (20 + get("_carnivorousPottedPlantWins")),
],
[$item`Red Roger's red left foot`, funPointValue],
[$item`PirateRealm party hat`, funPointValue],
]);
}
Loading