libram 0.4.3 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Clan.js CHANGED
@@ -9,6 +9,7 @@ import { getFoldGroup } from "./lib";
9
9
  import logger from "./logger";
10
10
  import { arrayToCountedMap, countedMapToArray, countedMapToString, notNull, parseNumber, } from "./utils";
11
11
  export class ClanError extends Error {
12
+ reason;
12
13
  constructor(message, reason) {
13
14
  super(message);
14
15
  this.reason = reason;
@@ -38,10 +39,8 @@ const toPlayerId = (player) => typeof player === "string" ? getPlayerId(player)
38
39
  const LOG_FAX_PATTERN = /(\d{2}\/\d{2}\/\d{2}, \d{2}:\d{2}(?:AM|PM): )<a [^>]+>([^<]+)<\/a>(?: faxed in a (?<monster>.*?))<br>/;
39
40
  const WHITELIST_DEGREE_PATTERN = /(?<name>.*?) \(°(?<degree>\d+)\)/;
40
41
  export class Clan {
41
- constructor(id, name) {
42
- this.id = id;
43
- this.name = name;
44
- }
42
+ id;
43
+ name;
45
44
  static _join(id) {
46
45
  const result = visitUrl(`showclan.php?recruiter=1&whichclan=${id}&pwd&whichclan=${id}&action=joinclan&apply=Apply+to+this+Clan&confirm=on`);
47
46
  if (!result.includes("clanhalltop.gif")) {
@@ -143,6 +142,10 @@ export class Clan {
143
142
  return new Clan(id, name);
144
143
  });
145
144
  }
145
+ constructor(id, name) {
146
+ this.id = id;
147
+ this.name = name;
148
+ }
146
149
  /**
147
150
  * Join clan
148
151
  */
package/dist/Copier.js CHANGED
@@ -1,6 +1,10 @@
1
1
  export class Copier {
2
+ couldCopy;
3
+ prepare;
4
+ canCopy;
5
+ copiedMonster;
6
+ fightCopy = null;
2
7
  constructor(couldCopy, prepare, canCopy, copiedMonster, fightCopy) {
3
- this.fightCopy = null;
4
8
  this.couldCopy = couldCopy;
5
9
  this.prepare = prepare;
6
10
  this.canCopy = canCopy;
package/dist/Kmail.js CHANGED
@@ -2,18 +2,12 @@ import "core-js/modules/es.object.entries";
2
2
  import { extractItems, extractMeat, isGiftable, toInt, visitUrl, } from "kolmafia";
3
3
  import { arrayToCountedMap, chunk } from "./utils";
4
4
  export default class Kmail {
5
- constructor(rawKmail) {
6
- const date = new Date(rawKmail.localtime);
7
- // Date come from KoL formatted with YY and so will be parsed 19YY, which is wrong.
8
- // We can safely add 100 because if 19YY was a leap year, 20YY will be too!
9
- date.setFullYear(date.getFullYear() + 100);
10
- this.id = Number(rawKmail.id);
11
- this.date = date;
12
- this.type = rawKmail.type;
13
- this.senderId = Number(rawKmail.fromid);
14
- this.senderName = rawKmail.fromname;
15
- this.rawMessage = rawKmail.message;
16
- }
5
+ id;
6
+ date;
7
+ type;
8
+ senderId;
9
+ senderName;
10
+ rawMessage;
17
11
  /**
18
12
  * Parses a kmail from KoL's native format
19
13
  *
@@ -99,6 +93,18 @@ export default class Kmail {
99
93
  const baseUrl = `town_sendgift.php?action=Yep.&pwd&fromwhere=0&note=${message}&insidenote=${insideNote}&towho=${to}`;
100
94
  return Kmail._genericSend(to, message, items, meat, 3, (m, itemsQuery, chunkSize) => `${baseUrl}&whichpackage=${chunkSize}${itemsQuery ? `&${itemsQuery}` : ""}&sendmeat=${m}`, ">Package sent.</");
101
95
  }
96
+ constructor(rawKmail) {
97
+ const date = new Date(rawKmail.localtime);
98
+ // Date come from KoL formatted with YY and so will be parsed 19YY, which is wrong.
99
+ // We can safely add 100 because if 19YY was a leap year, 20YY will be too!
100
+ date.setFullYear(date.getFullYear() + 100);
101
+ this.id = Number(rawKmail.id);
102
+ this.date = date;
103
+ this.type = rawKmail.type;
104
+ this.senderId = Number(rawKmail.fromid);
105
+ this.senderName = rawKmail.fromname;
106
+ this.rawMessage = rawKmail.message;
107
+ }
102
108
  /**
103
109
  * Delete the kmail
104
110
  *
package/dist/Path.js CHANGED
@@ -1,5 +1,14 @@
1
1
  import { $classes } from "./template-string";
2
2
  export class Path {
3
+ name;
4
+ id;
5
+ hasAllPerms; //here, we define avatar-ness around being its own class
6
+ hasCampground;
7
+ hasTerrarium;
8
+ stomachSize;
9
+ liverSize; //Defined as the lowest inebriety that makes you unable to drink more, just to make it fifteens across the board
10
+ spleenSize;
11
+ classes;
3
12
  /**
4
13
  *
5
14
  * @param name Name of path
package/dist/combat.d.ts CHANGED
@@ -120,9 +120,10 @@ export declare class Macro {
120
120
  * Create a new macro with a condition evaluated at the time of building the macro.
121
121
  * @param condition The JS condition.
122
122
  * @param ifTrue Continuation to add if the condition is true.
123
+ * @param ifFalse Optional input to turn this into an if...else statement.
123
124
  * @returns {Macro} This object itself.
124
125
  */
125
- static externalIf<T extends Macro>(this: Constructor<T>, condition: boolean, ifTrue: string | Macro): T;
126
+ static externalIf<T extends Macro>(this: Constructor<T>, condition: boolean, ifTrue: string | Macro, ifFalse?: string | Macro): T;
126
127
  /**
127
128
  * Add a repeat step to the macro.
128
129
  * @returns {Macro} This object itself.
@@ -198,6 +199,26 @@ export declare class Macro {
198
199
  * @returns {Macro} This object itself.
199
200
  */
200
201
  static attack<T extends Macro>(this: Constructor<T>): T;
202
+ /**
203
+ * Create an if_ statement based on what holiday of loathing it currently is. On non-holidays, returns the original macro, unmutated.
204
+ * @param macro The macro to place in the if_ statement
205
+ */
206
+ ifHolidayWanderer(macro: Macro): this;
207
+ /**
208
+ * Create a new macro starting with an ifHolidayWanderer step.
209
+ * @param macro The macro to place inside the if_ statement
210
+ */
211
+ static ifHolidayWanderer<T extends Macro>(this: Constructor<T>, macro: Macro): T;
212
+ /**
213
+ * Create an if_ statement based on what holiday of loathing it currently is. On non-holidays, returns the original macro, with the input macro appended.
214
+ * @param macro The macro to place in the if_ statement.
215
+ */
216
+ ifNotHolidayWanderer(macro: Macro): this;
217
+ /**
218
+ * Create a new macro starting with an ifNotHolidayWanderer step.
219
+ * @param macro The macro to place inside the if_ statement
220
+ */
221
+ static ifNotHolidayWanderer<T extends Macro>(this: Constructor<T>, macro: Macro): T;
201
222
  }
202
223
  /**
203
224
  * Adventure in a location and handle all combats with a given macro.
package/dist/combat.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { adv1, choiceFollowsFight, getAutoAttack, inMultiFight, removeProperty, runCombat, setAutoAttack, toInt, urlEncode, visitUrl, xpath, } from "kolmafia";
2
2
  import { $items, $skills } from "./template-string";
3
3
  import { get, set } from "./property";
4
+ import { getTodaysHolidayWanderers } from "./lib";
4
5
  const MACRO_NAME = "Script Autoattack Macro";
5
6
  /**
6
7
  * Get the KoL native ID of the macro with name Script Autoattack Macro.
@@ -68,9 +69,10 @@ export class InvalidMacroError extends Error {
68
69
  * For example, you can do `Macro.skill('Saucestorm').attack()`.
69
70
  */
70
71
  export class Macro {
71
- constructor() {
72
- this.components = [];
73
- }
72
+ static SAVED_MACRO_PROPERTY = "libram_savedMacro";
73
+ static cachedMacroId = null;
74
+ static cachedAutoAttack = null;
75
+ components = [];
74
76
  /**
75
77
  * Convert macro to string.
76
78
  */
@@ -256,10 +258,11 @@ export class Macro {
256
258
  * Create a new macro with a condition evaluated at the time of building the macro.
257
259
  * @param condition The JS condition.
258
260
  * @param ifTrue Continuation to add if the condition is true.
261
+ * @param ifFalse Optional input to turn this into an if...else statement.
259
262
  * @returns {Macro} This object itself.
260
263
  */
261
- static externalIf(condition, ifTrue) {
262
- return new this().externalIf(condition, ifTrue);
264
+ static externalIf(condition, ifTrue, ifFalse) {
265
+ return new this().externalIf(condition, ifTrue, ifFalse);
263
266
  }
264
267
  /**
265
268
  * Add a repeat step to the macro.
@@ -372,10 +375,41 @@ export class Macro {
372
375
  static attack() {
373
376
  return new this().attack();
374
377
  }
378
+ /**
379
+ * Create an if_ statement based on what holiday of loathing it currently is. On non-holidays, returns the original macro, unmutated.
380
+ * @param macro The macro to place in the if_ statement
381
+ */
382
+ ifHolidayWanderer(macro) {
383
+ const todaysWanderers = getTodaysHolidayWanderers();
384
+ if (todaysWanderers.length === 0)
385
+ return this;
386
+ return this.if_(todaysWanderers.map((monster) => `monsterid ${monster.id}`).join(" || "), macro);
387
+ }
388
+ /**
389
+ * Create a new macro starting with an ifHolidayWanderer step.
390
+ * @param macro The macro to place inside the if_ statement
391
+ */
392
+ static ifHolidayWanderer(macro) {
393
+ return new this().ifHolidayWanderer(macro);
394
+ }
395
+ /**
396
+ * Create an if_ statement based on what holiday of loathing it currently is. On non-holidays, returns the original macro, with the input macro appended.
397
+ * @param macro The macro to place in the if_ statement.
398
+ */
399
+ ifNotHolidayWanderer(macro) {
400
+ const todaysWanderers = getTodaysHolidayWanderers();
401
+ if (todaysWanderers.length === 0)
402
+ return this.step(macro);
403
+ return this.if_(todaysWanderers.map((monster) => `!monsterid ${monster.id}`).join(" && "), macro);
404
+ }
405
+ /**
406
+ * Create a new macro starting with an ifNotHolidayWanderer step.
407
+ * @param macro The macro to place inside the if_ statement
408
+ */
409
+ static ifNotHolidayWanderer(macro) {
410
+ return new this().ifNotHolidayWanderer(macro);
411
+ }
375
412
  }
376
- Macro.SAVED_MACRO_PROPERTY = "libram_savedMacro";
377
- Macro.cachedMacroId = null;
378
- Macro.cachedAutoAttack = null;
379
413
  /**
380
414
  * Adventure in a location and handle all combats with a given macro.
381
415
  * To use this function you will need to create a consult script that runs Macro.load().submit() and a CCS that calls that consult script.
@@ -13,6 +13,15 @@ export declare class MenuItem {
13
13
  additionalValue?: number;
14
14
  wishEffect?: Effect;
15
15
  static defaultOptions: Map<Item, MenuItemOptions>;
16
+ /**
17
+ * Construct a new menu item, possibly with extra properties. Items in MenuItem.defaultOptions have intelligent defaults.
18
+ * @param item Item to add to menu.
19
+ * @param options.organ Designate item as belonging to a specific organ.
20
+ * @param options.size Override item organ size. Necessary for any non-food/booze/spleen item.
21
+ * @param options.maximum Maximum uses remaining today, or "auto" to check dailyusesleft Mafia property.
22
+ * @param options.additionalValue Additional value (positive) or cost (negative) to consider with item, e.g. from buffs.
23
+ * @param options.wishEffect If item is a pocket wish, effect to wish for.
24
+ */
16
25
  constructor(item: Item, options?: MenuItemOptions);
17
26
  equals(other: MenuItem): boolean;
18
27
  toString(): string;
@@ -1,11 +1,15 @@
1
- import { fullnessLimit, getWorkshed, inebrietyLimit, itemType, mallPrice, mallPrices, myFullness, myPrimestat, mySpleenUse, npcPrice, spleenLimit, } from "kolmafia";
1
+ import { canEquip, fullnessLimit, getWorkshed, inebrietyLimit, itemType, mallPrice, mallPrices, myFullness, myInebriety, myLevel, myPrimestat, mySpleenUse, npcPrice, spleenLimit, } from "kolmafia";
2
2
  import { knapsack } from "./knapsack";
3
3
  import { have } from "../lib";
4
+ import { get as getModifier } from "../modifier";
4
5
  import { get } from "../property";
5
6
  import { $effect, $item, $items, $skill, $stat } from "../template-string";
6
7
  import { sum } from "../utils";
7
- // TODO: Include other consumption modifiers - Salty Mouth?
8
- // TODO: Include Gar-ish etc.
8
+ function isMonday() {
9
+ // Checking Tuesday's ruby is a hack to see if it's Monday in Arizona.
10
+ return getModifier("Muscle Percent", $item `Tuesday's ruby`) > 0;
11
+ }
12
+ // TODO: Include Salty Mouth and potentially other modifiers.
9
13
  function expectedAdventures(item, modifiers) {
10
14
  if (item.adventures === "")
11
15
  return 0;
@@ -18,6 +22,7 @@ function expectedAdventures(item, modifiers) {
18
22
  (itemType(item) === "booze" && item.notes?.includes("BEER"))
19
23
  ? 1.5
20
24
  : 1.3;
25
+ const garish = modifiers.garish && item.notes?.includes("LASAGNA") && !isMonday();
21
26
  const refinedPalate = modifiers.refinedPalate && item.notes?.includes("WINE");
22
27
  const pinkyRing = modifiers.pinkyRing && item.notes?.includes("WINE");
23
28
  return (sum(interpolated, (baseAdventures) => {
@@ -25,6 +30,11 @@ function expectedAdventures(item, modifiers) {
25
30
  if (modifiers.forkMug) {
26
31
  adventures = Math.floor(adventures * forkMugMultiplier);
27
32
  }
33
+ if (item.notes?.includes("SAUCY") && modifiers.saucemaven) {
34
+ adventures += myPrimestat() === $stat `Mysticality` ? 5 : 3;
35
+ }
36
+ if (garish)
37
+ adventures += 5;
28
38
  if (refinedPalate)
29
39
  adventures = Math.floor(adventures * 1.25);
30
40
  if (pinkyRing)
@@ -32,17 +42,81 @@ function expectedAdventures(item, modifiers) {
32
42
  if (item.notes?.includes("MARTINI") && modifiers.tuxedoShirt) {
33
43
  adventures += 2;
34
44
  }
35
- if (have($skill `Saucemaven`) && item.notes?.includes("SAUCY")) {
36
- adventures += myPrimestat() === $stat `Mysticality` ? 5 : 3;
37
- }
38
- if (itemType(item) === "food" && modifiers.seasoning)
39
- adventures++;
40
45
  if (itemType(item) === "food" && modifiers.mayoflex)
41
46
  adventures++;
47
+ if (itemType(item) === "food" && modifiers.seasoning)
48
+ adventures++;
42
49
  return adventures;
43
50
  }) / interpolated.length);
44
51
  }
45
52
  export class MenuItem {
53
+ item;
54
+ organ;
55
+ size;
56
+ maximum;
57
+ additionalValue;
58
+ wishEffect;
59
+ static defaultOptions = new Map([
60
+ [
61
+ $item `distention pill`,
62
+ {
63
+ organ: "food",
64
+ maximum: !have($item `distention pill`) || get("_distentionPillUsed") ? 0 : 1,
65
+ size: -1,
66
+ },
67
+ ],
68
+ [
69
+ $item `synthetic dog hair pill`,
70
+ {
71
+ organ: "booze",
72
+ maximum: !have($item `synthetic dog hair pill`) ||
73
+ get("_syntheticDogHairPillUsed")
74
+ ? 0
75
+ : 1,
76
+ size: -1,
77
+ },
78
+ ],
79
+ [
80
+ $item `cuppa Voraci tea`,
81
+ { organ: "food", maximum: get("_voraciTeaUsed") ? 0 : 1, size: -1 },
82
+ ],
83
+ [
84
+ $item `cuppa Sobrie tea`,
85
+ { organ: "booze", maximum: get("_sobrieTeaUsed") ? 0 : 1, size: -1 },
86
+ ],
87
+ [
88
+ $item `mojo filter`,
89
+ {
90
+ organ: "spleen item",
91
+ maximum: 3 - get("currentMojoFilters"),
92
+ size: -1,
93
+ },
94
+ ],
95
+ [$item `spice melange`, { maximum: get("spiceMelangeUsed") ? 0 : 1 }],
96
+ [
97
+ $item `Ultra Mega Sour Ball`,
98
+ { maximum: get("_ultraMegaSourBallUsed") ? 0 : 1 },
99
+ ],
100
+ [
101
+ $item `The Plumber's mushroom stew`,
102
+ { maximum: get("_plumbersMushroomStewEaten") ? 0 : 1 },
103
+ ],
104
+ [$item `The Mad Liquor`, { maximum: get("_madLiquorDrunk") ? 0 : 1 }],
105
+ [
106
+ $item `Doc Clock's thyme cocktail`,
107
+ { maximum: get("_docClocksThymeCocktailDrunk") ? 0 : 1 },
108
+ ],
109
+ [$item `Mr. Burnsger`, { maximum: get("_mrBurnsgerEaten") ? 0 : 1 }],
110
+ ]);
111
+ /**
112
+ * Construct a new menu item, possibly with extra properties. Items in MenuItem.defaultOptions have intelligent defaults.
113
+ * @param item Item to add to menu.
114
+ * @param options.organ Designate item as belonging to a specific organ.
115
+ * @param options.size Override item organ size. Necessary for any non-food/booze/spleen item.
116
+ * @param options.maximum Maximum uses remaining today, or "auto" to check dailyusesleft Mafia property.
117
+ * @param options.additionalValue Additional value (positive) or cost (negative) to consider with item, e.g. from buffs.
118
+ * @param options.wishEffect If item is a pocket wish, effect to wish for.
119
+ */
46
120
  constructor(item, options = {}) {
47
121
  const { size, organ, maximum, additionalValue, wishEffect } = {
48
122
  ...options,
@@ -74,38 +148,19 @@ export class MenuItem {
74
148
  return npcPrice(this.item) > 0 ? npcPrice(this.item) : mallPrice(this.item);
75
149
  }
76
150
  }
77
- MenuItem.defaultOptions = new Map([
78
- [$item `Mr. Burnsger`, { maximum: "auto" }],
79
- [
80
- $item `distention pill`,
81
- {
82
- organ: "food",
83
- maximum: "auto",
84
- size: -1,
85
- },
86
- ],
87
- [
88
- $item `synthetic dog hair pill`,
89
- { organ: "booze", maximum: "auto", size: -1 },
90
- ],
91
- [$item `cuppa Voraci tea`, { organ: "food", maximum: "auto", size: -1 }],
92
- [$item `cuppa Sobrie tea`, { organ: "booze", maximum: "auto", size: -1 }],
93
- [
94
- $item `mojo filter`,
95
- {
96
- organ: "spleen item",
97
- maximum: 3 - get("currentMojoFilters"),
98
- size: -1,
99
- },
100
- ],
101
- ]);
102
151
  const organs = ["food", "booze", "spleen item"];
103
152
  function isOrgan(x) {
104
153
  return organs.includes(x);
105
154
  }
106
155
  class DietPlanner {
156
+ mpa;
157
+ menu;
158
+ fork;
159
+ mug;
160
+ seasoning;
161
+ mayoflex;
162
+ spleenValue = 0;
107
163
  constructor(mpa, menu) {
108
- this.spleenValue = 0;
109
164
  this.mpa = mpa;
110
165
  this.fork = menu.find((item) => item.item === $item `Ol' Scratch's salad fork`);
111
166
  this.mug = menu.find((item) => item.item === $item `Frosty's frosty mug`);
@@ -114,8 +169,6 @@ class DietPlanner {
114
169
  getWorkshed() === $item `portable Mayo Clinic`
115
170
  ? menu.find((item) => item.item === $item `Mayoflex`)
116
171
  : undefined;
117
- this.pinkyRing = have($item `mafia pinky ring`);
118
- this.tuxedoShirt = have($item `tuxedo shirt`);
119
172
  this.menu = menu.filter((item) => item.organ);
120
173
  if (menu.length > 100) {
121
174
  mallPrices("food");
@@ -129,9 +182,20 @@ class DietPlanner {
129
182
  this.consumptionValue(spleenItems[0]) / spleenItems[0].item.spleen;
130
183
  }
131
184
  }
185
+ /**
186
+ * Determine the value of consuming a menu item with any profitable helpers.
187
+ * @param menuItem Menu item to check.
188
+ * @returns Value for consuming that menu item.
189
+ */
132
190
  consumptionValue(menuItem) {
133
191
  return this.consumptionHelpersAndValue(menuItem, {})[1];
134
192
  }
193
+ /**
194
+ * Determine which helpers will be used with a menu item and its resulting value.
195
+ * @param menuItem Menu item to check.
196
+ * @param overrideModifiers Overrides for consumption modifiers, if any.
197
+ * @returns Pair [array of helpers and base menu item, value].
198
+ */
135
199
  consumptionHelpersAndValue(menuItem, overrideModifiers) {
136
200
  const helpers = [];
137
201
  if (this.seasoning &&
@@ -148,9 +212,11 @@ class DietPlanner {
148
212
  forkMug: false,
149
213
  seasoning: this.seasoning ? helpers.includes(this.seasoning) : false,
150
214
  mayoflex: this.mayoflex ? helpers.includes(this.mayoflex) : false,
151
- refinedPalate: false,
152
- pinkyRing: this.pinkyRing,
153
- tuxedoShirt: this.tuxedoShirt,
215
+ refinedPalate: have($effect `Refined Palate`),
216
+ garish: have($effect `Gar-ish`),
217
+ saucemaven: have($skill `Saucemaven`),
218
+ pinkyRing: have($item `mafia pinky ring`) && canEquip($item `mafia pinky ring`),
219
+ tuxedoShirt: have($item `tuxedo shirt`) && canEquip($item `tuxedo shirt`),
154
220
  ...overrideModifiers,
155
221
  };
156
222
  const forkMug = itemType(menuItem.item) === "food"
@@ -178,8 +244,14 @@ class DietPlanner {
178
244
  ? [[...helpers, forkMug, menuItem], valueForkMug + valueSpleen]
179
245
  : [[...helpers, menuItem], valueRaw + valueSpleen];
180
246
  }
247
+ /**
248
+ * Plan an individual organ.
249
+ * @param capacity Organ capacity.
250
+ * @param overrideModifiers Overrides for consumption modifiers, if any.
251
+ * @returns Pair of [value, menu items and quantities].
252
+ */
181
253
  planOrgan(organ, capacity, overrideModifiers = {}) {
182
- const submenu = this.menu.filter((item) => item.organ === organ);
254
+ const submenu = this.menu.filter((menuItem) => menuItem.organ === organ && myLevel() >= menuItem.item.levelreq);
183
255
  const knapsackValues = submenu.map((menuItem) => [
184
256
  ...this.consumptionHelpersAndValue(menuItem, overrideModifiers),
185
257
  menuItem.size,
@@ -187,6 +259,12 @@ class DietPlanner {
187
259
  ]);
188
260
  return knapsack(knapsackValues, capacity);
189
261
  }
262
+ /**
263
+ * Plan organs.
264
+ * @param organCapacities Organ capacities.
265
+ * @param overrideModifiers Overrides for consumption modifiers, if any.
266
+ * @returns Pair of [value, menu items and quantities].
267
+ */
190
268
  planOrgans(organCapacities, overrideModifiers = {}) {
191
269
  const valuePlans = organCapacities.map(([organ, capacity]) => this.planOrgan(organ, capacity, overrideModifiers));
192
270
  return [
@@ -194,12 +272,23 @@ class DietPlanner {
194
272
  [].concat(...valuePlans.map(([, plan]) => plan)),
195
273
  ];
196
274
  }
197
- planOrgansWithTrials(organCapacities, trialItems, overrideModifiers = {}) {
275
+ /**
276
+ * Plan organs, retrying with and without each trial item. Runtime is
277
+ * proportional to 2 ^ trialItems.length.
278
+ * @param organCapacities Organ capacities.
279
+ * @param trialItems Items to rerun solver with and without.
280
+ * @param overrideModifiers Overrides for consumption modifiers, if any.
281
+ * @returns Pair of [value, menu items and quantities].
282
+ */
283
+ planOrgansWithTrials(organCapacities, trialItems, overrideModifiers) {
198
284
  if (trialItems.length === 0) {
199
285
  return this.planOrgans(organCapacities, overrideModifiers);
200
286
  }
201
- const organCapacitiesWithMap = new Map(organCapacities);
202
287
  const [trialItem, organSizes] = trialItems[0];
288
+ if (trialItem.maximum !== undefined && trialItem.maximum <= 0) {
289
+ return this.planOrgansWithTrials(organCapacities, trialItems.slice(1), overrideModifiers);
290
+ }
291
+ const organCapacitiesWithMap = new Map(organCapacities);
203
292
  for (const [organ, size] of organSizes) {
204
293
  const current = organCapacitiesWithMap.get(organ);
205
294
  if (current !== undefined) {
@@ -207,12 +296,18 @@ class DietPlanner {
207
296
  }
208
297
  }
209
298
  const organCapacitiesWith = [...organCapacitiesWithMap];
210
- const isRefinedPalate = trialItem.item === $item `pocket wish` &&
211
- trialItem.wishEffect === $effect `Refined Palate`;
299
+ const isRefinedPalate = (trialItem.item === $item `pocket wish` &&
300
+ trialItem.wishEffect === $effect `Refined Palate`) ||
301
+ trialItem.item === $item `toasted brie`;
302
+ const isGarish = (trialItem.item === $item `pocket wish` &&
303
+ trialItem.wishEffect === $effect `Gar-ish`) ||
304
+ trialItem.item === $item `potion of the field gar`;
212
305
  const [valueWithout, planWithout] = this.planOrgansWithTrials(organCapacities, trialItems.slice(1), overrideModifiers);
213
- const [valueWith, planWith] = this.planOrgansWithTrials(organCapacitiesWith, trialItems.slice(1), isRefinedPalate
214
- ? { ...overrideModifiers, refinedPalate: true }
215
- : overrideModifiers);
306
+ const [valueWith, planWith] = this.planOrgansWithTrials(organCapacitiesWith, trialItems.slice(1), {
307
+ ...overrideModifiers,
308
+ ...(isRefinedPalate ? { refinedPalate: true } : {}),
309
+ ...(isGarish ? { garish: true } : {}),
310
+ });
216
311
  const [helpersAndItem, value] = this.consumptionHelpersAndValue(trialItem, {});
217
312
  return valueWithout > valueWith + value
218
313
  ? [valueWithout, planWithout]
@@ -267,6 +362,10 @@ const interactingItems = [
267
362
  ["booze", -2],
268
363
  ],
269
364
  ],
365
+ [$effect `Refined Palate`, []],
366
+ [$item `toasted brie`, [["food", 2]]],
367
+ [$effect `Gar-ish`, []],
368
+ [$item `potion of the field gar`, []],
270
369
  ];
271
370
  /**
272
371
  * Plan out an optimal diet using a knapsack algorithm.
@@ -280,37 +379,58 @@ export function planDiet(mpa, menu, organCapacities = [
280
379
  ["booze", null],
281
380
  ["spleen item", null],
282
381
  ]) {
283
- const dietPlanner = new DietPlanner(mpa, menu);
382
+ // FIXME: Figure out a better way to handle overfull organs (e.g. coming out of Ed).
284
383
  const resolvedOrganCapacities = organCapacities.map(([organ, size]) => [
285
384
  organ,
286
385
  size ??
287
386
  (organ === "food"
288
- ? fullnessLimit() -
289
- myFullness() +
290
- (have($item `distention pill`) ? 1 : 0)
387
+ ? fullnessLimit() - myFullness()
291
388
  : organ === "booze"
292
- ? inebrietyLimit() + (have($item `synthetic dog hair pill`) ? 1 : 0)
389
+ ? inebrietyLimit() - myInebriety()
293
390
  : organ === "spleen item"
294
391
  ? spleenLimit() - mySpleenUse()
295
392
  : 0),
296
393
  ]);
297
- const allItems = new Map(menu.map((menuItem) => [menuItem.item, menuItem]));
298
- const includedInteractingItems = interactingItems
299
- .map(([item, sizes]) => [allItems.get(item), sizes])
300
- .filter(([menuItem]) => menuItem);
301
- // TODO: support toasted brie.
302
- // Refined Palate must also be treated as an interacting item, as it's a one-time cost.
303
- const palateWish = menu.find((menuItem) => menuItem.item === $item `pocket wish` &&
304
- menuItem.wishEffect === $effect `Refined Palate`);
305
- if (palateWish) {
306
- includedInteractingItems.push([palateWish, []]);
394
+ /**
395
+ * Per above description, separate out items with cross-organ interaction
396
+ * ("interacting items") for special treatment. These will be checked by
397
+ * running the solver several times.
398
+ */
399
+ const includedInteractingItems = menu
400
+ .map((menuItem) => {
401
+ const interacting = interactingItems.find(([itemOrEffect]) => menuItem.item === itemOrEffect ||
402
+ (menuItem.item === $item `pocket wish` &&
403
+ menuItem.wishEffect === itemOrEffect));
404
+ if (interacting) {
405
+ const [, organSizes] = interacting;
406
+ return [menuItem, organSizes];
407
+ }
408
+ else {
409
+ return null;
410
+ }
411
+ })
412
+ .filter((value) => value !== null);
413
+ // Filter out interacting items from natural consideration.
414
+ const dietPlanner = new DietPlanner(mpa, menu.filter((menuItem) => !includedInteractingItems.some(([interacting]) => interacting === menuItem)));
415
+ /**
416
+ * Because our knapsack solver is one-dimensional, we have to consider
417
+ * each organ separately. Since there are no spleen items that affect
418
+ * stomach/liver, we consider those two first, with an approximation of the
419
+ * value of spleen-cleaning. Afterwards, we see how much spleen we have and
420
+ * plan that.
421
+ */
422
+ const [, planFoodBooze] = dietPlanner.planOrgansWithTrials(resolvedOrganCapacities.filter(([organ, capacity]) => ["food", "booze"].includes(organ) && capacity >= 0), includedInteractingItems, {});
423
+ const spleenCapacity = resolvedOrganCapacities.find(([organ]) => organ === "spleen item");
424
+ if (spleenCapacity) {
425
+ // Count sliders and pickle juice, figure out how much extra spleen we got.
426
+ const additionalSpleen = sum(planFoodBooze, ([items, number]) => items.some((menuItem) => $items `jar of fermented pickle juice, extra-greasy slider`.includes(menuItem.item))
427
+ ? 5 * number
428
+ : 0);
429
+ const [, availableSpleen] = spleenCapacity;
430
+ const [, planSpleen] = dietPlanner.planOrgan("spleen item", availableSpleen + additionalSpleen);
431
+ return [...planFoodBooze, ...planSpleen];
432
+ }
433
+ else {
434
+ return planFoodBooze;
307
435
  }
308
- const [, planFoodBooze] = dietPlanner.planOrgansWithTrials(resolvedOrganCapacities.filter(([organ]) => ["food", "booze"].includes(organ)), includedInteractingItems);
309
- // Count sliders and pickle juice, figure out how much extra spleen we got.
310
- const additionalSpleen = sum(planFoodBooze, ([items, number]) => items.some((menuItem) => $items `jar of fermented pickle juice, extra-greasy slider`.includes(menuItem.item))
311
- ? 5 * number
312
- : 0);
313
- const [, availableSpleen] = resolvedOrganCapacities.find(([organ]) => organ === "spleen item") ?? ["spleen item", 0];
314
- const [, planSpleen] = dietPlanner.planOrgan("spleen item", availableSpleen + additionalSpleen);
315
- return [...planFoodBooze, ...planSpleen];
316
436
  }
@@ -1,5 +1,6 @@
1
1
  import { sum } from "../utils";
2
2
  class Not {
3
+ thing;
3
4
  constructor(thing) {
4
5
  this.thing = thing;
5
6
  }
@@ -32,17 +33,30 @@ function aggregate(list, isEqual) {
32
33
  * @returns Tuple {[totalValue, items]} of selected items and total value of those items.
33
34
  */
34
35
  export function knapsack(values, capacity) {
36
+ if (!Number.isFinite(capacity)) {
37
+ throw new Error("Invalid capacity.");
38
+ }
35
39
  // Invert negative values into a fake value for not using it.
36
- const valuesInverted = values.map(([thing, value, weight, maximum]) => (weight < 0 && maximum
40
+ const valuesInverted = values.map(([thing, value, weight, maximum]) => (weight < 0 && maximum !== undefined
37
41
  ? [new Not(thing), -value, -weight, maximum]
38
42
  : [thing, value, weight, maximum]));
39
- const capacityAdjustment = sum(values, ([, , weight, maximum]) => weight < 0 && maximum ? -weight * maximum : 0);
43
+ const capacityAdjustment = sum(values, ([, , weight, maximum]) => weight < 0 && maximum !== undefined ? -weight * maximum : 0);
40
44
  const adjustedCapacity = capacity + capacityAdjustment;
45
+ if (adjustedCapacity < 0) {
46
+ // We don't have enough cleaners to create any space, so can't fit anything.
47
+ return [-Infinity, []];
48
+ }
41
49
  // Sort values by weight.
42
50
  const valuesSorted = [...valuesInverted].sort((x, y) => x[2] - y[2]);
43
51
  // Convert the problem into 0/1 knapsack - just include as many copies as possible of each item.
44
52
  const values01 = [].concat(...valuesSorted.map(([thing, value, weight, maximum]) => {
53
+ if (!Number.isFinite(weight) || weight < 0) {
54
+ throw new Error(`Invalid weight ${weight} for ${thing instanceof Not ? `not ${thing.thing}` : thing}`);
55
+ }
45
56
  const maxQuantity = maximum ?? Math.floor(adjustedCapacity / weight);
57
+ if (maxQuantity < 0) {
58
+ throw new Error(`Invalid max quantity ${maxQuantity} for ${thing instanceof Not ? `not ${thing.thing}` : thing}`);
59
+ }
46
60
  return new Array(maxQuantity).fill([
47
61
  thing,
48
62
  value,