farming-weight 0.10.7 → 0.11.0

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.
Files changed (105) hide show
  1. package/dist/constants/attributes.d.ts +2 -1
  2. package/dist/constants/attributes.js +9 -3
  3. package/dist/constants/attributes.js.map +1 -1
  4. package/dist/constants/crops.d.ts +4 -1
  5. package/dist/constants/crops.js +62 -2
  6. package/dist/constants/crops.js.map +1 -1
  7. package/dist/constants/garden.js +18 -0
  8. package/dist/constants/garden.js.map +1 -1
  9. package/dist/constants/gems.d.ts +14 -1
  10. package/dist/constants/gems.js +561 -42
  11. package/dist/constants/gems.js.map +1 -1
  12. package/dist/constants/personalbests.js +4 -1
  13. package/dist/constants/personalbests.js.map +1 -1
  14. package/dist/constants/pests.d.ts +4 -1
  15. package/dist/constants/pests.js +119 -65
  16. package/dist/constants/pests.js.map +1 -1
  17. package/dist/constants/reforges.js +4 -0
  18. package/dist/constants/reforges.js.map +1 -1
  19. package/dist/constants/specialcrops.d.ts +15 -1
  20. package/dist/constants/specialcrops.js +17 -0
  21. package/dist/constants/specialcrops.js.map +1 -1
  22. package/dist/constants/specific.d.ts +1 -0
  23. package/dist/constants/specific.js +10 -0
  24. package/dist/constants/specific.js.map +1 -1
  25. package/dist/constants/stats.d.ts +5 -1
  26. package/dist/constants/stats.js +4 -0
  27. package/dist/constants/stats.js.map +1 -1
  28. package/dist/constants/upgrades.d.ts +43 -0
  29. package/dist/constants/upgrades.js.map +1 -1
  30. package/dist/constants/weight.js +5 -2
  31. package/dist/constants/weight.js.map +1 -1
  32. package/dist/fortune/farmingaccessory.d.ts +3 -1
  33. package/dist/fortune/farmingaccessory.js +17 -5
  34. package/dist/fortune/farmingaccessory.js.map +1 -1
  35. package/dist/fortune/farmingarmor.d.ts +14 -5
  36. package/dist/fortune/farmingarmor.js +123 -17
  37. package/dist/fortune/farmingarmor.js.map +1 -1
  38. package/dist/fortune/farmingequipment.d.ts +4 -2
  39. package/dist/fortune/farmingequipment.js +27 -5
  40. package/dist/fortune/farmingequipment.js.map +1 -1
  41. package/dist/fortune/farmingtool.d.ts +8 -4
  42. package/dist/fortune/farmingtool.js +61 -15
  43. package/dist/fortune/farmingtool.js.map +1 -1
  44. package/dist/fortune/upgradeable.d.ts +7 -2
  45. package/dist/fortune/upgradeablebase.d.ts +7 -3
  46. package/dist/fortune/upgradeablebase.js +20 -4
  47. package/dist/fortune/upgradeablebase.js.map +1 -1
  48. package/dist/items/accessories.d.ts +3 -14
  49. package/dist/items/accessories.js +9 -0
  50. package/dist/items/accessories.js.map +1 -1
  51. package/dist/items/armor.d.ts +3 -35
  52. package/dist/items/armor.js +139 -63
  53. package/dist/items/armor.js.map +1 -1
  54. package/dist/items/definitions.d.ts +44 -0
  55. package/dist/items/definitions.js +62 -0
  56. package/dist/items/definitions.js.map +1 -0
  57. package/dist/items/equipment.d.ts +2 -2
  58. package/dist/items/equipment.js +1 -1
  59. package/dist/items/equipment.js.map +1 -1
  60. package/dist/items/tools.d.ts +3 -14
  61. package/dist/items/tools.js +6 -36
  62. package/dist/items/tools.js.map +1 -1
  63. package/dist/player/player.d.ts +45 -4
  64. package/dist/player/player.js +720 -94
  65. package/dist/player/player.js.map +1 -1
  66. package/dist/player/playeroptions.d.ts +1 -0
  67. package/dist/player/playeroptions.js.map +1 -1
  68. package/dist/upgrades/enchantupgrades.d.ts +3 -2
  69. package/dist/upgrades/enchantupgrades.js +48 -8
  70. package/dist/upgrades/enchantupgrades.js.map +1 -1
  71. package/dist/upgrades/getfortune.d.ts +2 -1
  72. package/dist/upgrades/getfortune.js +7 -2
  73. package/dist/upgrades/getfortune.js.map +1 -1
  74. package/dist/upgrades/getsourceprogress.d.ts +2 -1
  75. package/dist/upgrades/getsourceprogress.js +53 -3
  76. package/dist/upgrades/getsourceprogress.js.map +1 -1
  77. package/dist/upgrades/sources/armorsetsources.js +57 -27
  78. package/dist/upgrades/sources/armorsetsources.js.map +1 -1
  79. package/dist/upgrades/sources/cropsources.js +39 -3
  80. package/dist/upgrades/sources/cropsources.js.map +1 -1
  81. package/dist/upgrades/sources/dynamicfortunesources.d.ts +10 -2
  82. package/dist/upgrades/sources/gearsources.js +54 -5
  83. package/dist/upgrades/sources/gearsources.js.map +1 -1
  84. package/dist/upgrades/sources/generalsources.js +177 -8
  85. package/dist/upgrades/sources/generalsources.js.map +1 -1
  86. package/dist/upgrades/sources/toolsources.js +108 -17
  87. package/dist/upgrades/sources/toolsources.js.map +1 -1
  88. package/dist/upgrades/upgrades.d.ts +5 -2
  89. package/dist/upgrades/upgrades.js +114 -24
  90. package/dist/upgrades/upgrades.js.map +1 -1
  91. package/dist/upgrades/upgradeutils.d.ts +6 -0
  92. package/dist/upgrades/upgradeutils.js +18 -0
  93. package/dist/upgrades/upgradeutils.js.map +1 -0
  94. package/dist/util/enchants.d.ts +3 -0
  95. package/dist/util/enchants.js +18 -12
  96. package/dist/util/enchants.js.map +1 -1
  97. package/dist/util/garden.d.ts +1 -0
  98. package/dist/util/garden.js +8 -2
  99. package/dist/util/garden.js.map +1 -1
  100. package/dist/util/gems.d.ts +2 -0
  101. package/dist/util/gems.js +29 -8
  102. package/dist/util/gems.js.map +1 -1
  103. package/dist/util/names.js +23 -0
  104. package/dist/util/names.js.map +1 -1
  105. package/package.json +3 -2
@@ -1,7 +1,8 @@
1
- import { FARMING_ATTRIBUTE_SHARDS, getShardFortune } from '../constants/attributes.js';
1
+ import { FARMING_ATTRIBUTE_SHARDS, getShardStat } from '../constants/attributes.js';
2
2
  import { CROP_INFO, Crop, EXPORTABLE_CROP_FORTUNE } from '../constants/crops.js';
3
3
  import { fortuneFromPersonalBestContest } from '../constants/personalbests.js';
4
- import { ANITA_FORTUNE_UPGRADE, COCOA_FORTUNE_UPGRADE, COMMUNITY_CENTER_UPGRADE, FARMING_LEVEL, GARDEN_CROP_UPGRADES, UNLOCKED_PLOTS, } from '../constants/specific.js';
4
+ import { ANITA_FORTUNE_UPGRADE, COCOA_FORTUNE_UPGRADE, COMMUNITY_CENTER_UPGRADE, FARMING_LEVEL, GARDEN_CROP_UPGRADES, UNLOCKED_PLOTS, WRIGGLING_LARVA_SOURCE, } from '../constants/specific.js';
5
+ import { Stat } from '../constants/stats.js';
5
6
  import { TEMPORARY_FORTUNE } from '../constants/tempfortune.js';
6
7
  import { UpgradeAction, UpgradeCategory } from '../constants/upgrades.js';
7
8
  import { FarmingAccessory } from '../fortune/farmingaccessory.js';
@@ -16,6 +17,7 @@ import { getSourceProgress } from '../upgrades/getsourceprogress.js';
16
17
  import { getFakeItem } from '../upgrades/itemregistry.js';
17
18
  import { CROP_FORTUNE_SOURCES } from '../upgrades/sources/cropsources.js';
18
19
  import { GENERAL_FORTUNE_SOURCES } from '../upgrades/sources/generalsources.js';
20
+ import { filterAndSortUpgrades } from '../upgrades/upgradeutils.js';
19
21
  import { getCropDisplayName, getItemIdFromCrop } from '../util/names.js';
20
22
  import { fortuneFromPestBestiary } from '../util/pests.js';
21
23
  import { calculateDetailedDrops } from '../util/ratecalc.js';
@@ -31,70 +33,96 @@ export class FarmingPlayer {
31
33
  return this.options.attributes ?? {};
32
34
  }
33
35
  constructor(options) {
36
+ this.setOptions(options);
37
+ }
38
+ setOptions(options) {
34
39
  this.options = options;
35
- options.pets ??= [];
36
- if (options.pets[0] instanceof FarmingPet) {
37
- this.pets = options.pets.sort((a, b) => b.fortune - a.fortune);
40
+ this.populatePets();
41
+ this.populateTools();
42
+ this.populateArmor();
43
+ this.populateEquipment();
44
+ this.populateActiveAccessories();
45
+ this.permFortune = this.getGeneralFortune();
46
+ this.tempFortune = this.getTempFortune();
47
+ }
48
+ populatePets() {
49
+ this.options.pets ??= [];
50
+ if (this.options.pets[0] instanceof FarmingPet) {
51
+ this.pets = this.options.pets.sort((a, b) => b.fortune - a.fortune);
38
52
  for (const pet of this.pets)
39
- pet.setOptions(options);
53
+ pet.setOptions(this.options);
40
54
  }
41
55
  else {
42
- this.pets = FarmingPet.fromArray(options.pets, options);
56
+ this.pets = FarmingPet.fromArray(this.options.pets, this.options);
43
57
  }
44
58
  this.selectedPet = this.options.selectedPet;
45
- options.tools ??= [];
46
- if (options.tools[0] instanceof FarmingTool) {
47
- this.tools = options.tools;
59
+ }
60
+ populateTools() {
61
+ this.options.tools ??= [];
62
+ if (this.options.tools[0] instanceof FarmingTool) {
63
+ this.tools = this.options.tools;
48
64
  for (const tool of this.tools)
49
- tool.setOptions(options);
65
+ tool.setOptions(this.options);
50
66
  this.tools.sort((a, b) => b.fortune - a.fortune);
51
67
  }
52
68
  else {
53
- this.tools = FarmingTool.fromArray(options.tools, options);
69
+ this.tools = FarmingTool.fromArray(this.options.tools, this.options);
54
70
  }
55
71
  this.selectedTool = this.options.selectedTool ?? this.tools[0];
56
- options.armor ??= [];
57
- if (options.armor instanceof ArmorSet) {
58
- this.armorSet = options.armor;
72
+ }
73
+ populateArmor() {
74
+ this.options.armor ??= [];
75
+ if (this.options.armor instanceof ArmorSet) {
76
+ this.armorSet = this.options.armor;
59
77
  this.armor = this.armorSet.pieces;
60
78
  this.armor.sort((a, b) => b.potential - a.potential);
61
79
  this.equipment = this.armorSet.equipmentPieces;
62
80
  this.equipment.sort((a, b) => b.fortune - a.fortune);
63
- this.armorSet.setOptions(options);
81
+ this.armorSet.setOptions(this.options);
64
82
  }
65
- else if (options.armor[0] instanceof FarmingArmor) {
66
- this.armor = options.armor.sort((a, b) => b.potential - a.potential);
83
+ else if (this.options.armor[0] instanceof FarmingArmor) {
84
+ this.armor = this.options.armor.sort((a, b) => b.potential - a.potential);
67
85
  for (const a of this.armor)
68
- a.setOptions(options);
86
+ a.setOptions(this.options);
69
87
  this.armorSet = new ArmorSet(this.armor);
70
88
  }
71
89
  else {
72
- this.armor = FarmingArmor.fromArray(options.armor, options);
90
+ this.armor = FarmingArmor.fromArray(this.options.armor, this.options);
73
91
  this.armorSet = new ArmorSet(this.armor);
74
92
  }
75
- options.equipment ??= [];
76
- if (options.equipment[0] instanceof FarmingEquipment) {
77
- this.equipment = options.equipment.sort((a, b) => b.fortune - a.fortune);
93
+ }
94
+ populateEquipment() {
95
+ this.options.equipment ??= [];
96
+ // If equipment was already set by populateArmor (from ArmorSet), don't overwrite it
97
+ if (this.equipment && this.equipment.length > 0) {
98
+ // Load in equipment to armor set if it's empty
99
+ if (this.armorSet.equipment.filter((e) => e).length === 0) {
100
+ this.armorSet.setEquipment(this.equipment);
101
+ }
102
+ return;
103
+ }
104
+ if (this.options.equipment[0] instanceof FarmingEquipment) {
105
+ this.equipment = this.options.equipment.sort((a, b) => b.fortune - a.fortune);
78
106
  for (const e of this.equipment)
79
- e.setOptions(options);
107
+ e.setOptions(this.options);
80
108
  }
81
109
  else {
82
- this.equipment = FarmingEquipment.fromArray(options.equipment, options);
110
+ this.equipment = FarmingEquipment.fromArray(this.options.equipment, this.options);
83
111
  }
84
112
  // Load in equipment to armor set if it's empty
85
113
  if (this.armorSet.equipment.filter((e) => e).length === 0) {
86
114
  this.armorSet.setEquipment(this.equipment);
87
115
  }
88
- options.accessories ??= [];
116
+ }
117
+ populateActiveAccessories() {
118
+ this.options.accessories ??= [];
89
119
  this.activeAccessories = [];
90
- if (options.accessories[0] instanceof FarmingAccessory) {
91
- this.accessories = options.accessories.sort((a, b) => b.fortune - a.fortune);
120
+ if (this.options.accessories[0] instanceof FarmingAccessory) {
121
+ this.accessories = this.options.accessories.sort((a, b) => b.fortune - a.fortune);
92
122
  }
93
123
  else {
94
- this.accessories = FarmingAccessory.fromArray(options.accessories);
124
+ this.accessories = FarmingAccessory.fromArray(this.options.accessories);
95
125
  }
96
- this.permFortune = this.getGeneralFortune();
97
- this.tempFortune = this.getTempFortune();
98
126
  }
99
127
  changeArmor(armor) {
100
128
  this.armorSet = new ArmorSet(armor.sort((a, b) => b.fortune - a.fortune));
@@ -114,17 +142,17 @@ export class FarmingPlayer {
114
142
  }
115
143
  this.permFortune = this.getGeneralFortune();
116
144
  }
117
- getProgress() {
118
- return getSourceProgress(this, GENERAL_FORTUNE_SOURCES);
145
+ getProgress(stats) {
146
+ return getSourceProgress(this, GENERAL_FORTUNE_SOURCES, false, stats);
119
147
  }
120
- getUpgrades() {
121
- const upgrades = getSourceProgress(this, GENERAL_FORTUNE_SOURCES).flatMap((source) => source.upgrades ?? []);
122
- const armorSetUpgrades = this.armorSet.getUpgrades();
148
+ getUpgrades(options) {
149
+ const stats = options?.stat ? [options.stat] : undefined;
150
+ const upgrades = getSourceProgress(this, GENERAL_FORTUNE_SOURCES, false, stats).flatMap((source) => source.upgrades ?? []);
151
+ const armorSetUpgrades = this.armorSet.getUpgrades(options);
123
152
  if (armorSetUpgrades.length > 0) {
124
153
  upgrades.push(...armorSetUpgrades);
125
154
  }
126
- upgrades.sort((a, b) => b.increase - a.increase);
127
- return upgrades;
155
+ return filterAndSortUpgrades(upgrades, options);
128
156
  }
129
157
  getCropUpgrades(crop, tool) {
130
158
  const upgrades = [];
@@ -166,111 +194,160 @@ export class FarmingPlayer {
166
194
  return upgrades;
167
195
  }
168
196
  getGeneralFortune() {
197
+ const { value, breakdown } = this.getStatBreakdown(Stat.FarmingFortune);
198
+ this.breakdown = breakdown;
199
+ return value;
200
+ }
201
+ getStat(stat) {
202
+ return this.getStatBreakdown(stat).value;
203
+ }
204
+ getStatBreakdown(stat) {
169
205
  let sum = 0;
170
206
  const breakdown = {};
171
207
  // Plots
172
- const plots = getFortune(this.options.plots?.length ?? this.options.plotsUnlocked, UNLOCKED_PLOTS);
208
+ const plots = getFortune(this.options.plots?.length ?? this.options.plotsUnlocked, UNLOCKED_PLOTS, stat);
173
209
  if (plots > 0) {
174
210
  breakdown['Unlocked Plots'] = plots;
175
211
  sum += plots;
176
212
  }
177
213
  // Farming Level
178
- const level = getFortune(this.options.farmingLevel, FARMING_LEVEL);
214
+ const level = getFortune(this.options.farmingLevel, FARMING_LEVEL, stat);
179
215
  if (level > 0) {
180
216
  breakdown['Farming Level'] = level;
181
217
  sum += level;
182
218
  }
183
219
  // Bestiary
184
- if (this.options.bestiaryKills) {
220
+ if (stat === Stat.FarmingFortune && this.options.bestiaryKills) {
185
221
  const bestiary = fortuneFromPestBestiary(this.options.bestiaryKills);
186
222
  if (bestiary > 0) {
187
223
  breakdown['Pest Bestiary'] = bestiary;
188
224
  sum += bestiary;
189
225
  }
190
226
  }
191
- // Armor Set
192
- const armorSet = this.armorSet.armorFortune;
193
- if (armorSet > 0) {
194
- breakdown['Armor Set'] = armorSet;
195
- sum += armorSet;
227
+ // Armor pieces
228
+ for (const piece of this.armorSet.armor) {
229
+ if (!piece)
230
+ continue;
231
+ const val = piece.getStat(stat);
232
+ if (val > 0) {
233
+ breakdown[piece.item.name ?? 'Armor Piece'] = val;
234
+ sum += val;
235
+ }
236
+ }
237
+ // Armor Set Bonuses
238
+ for (const { bonus, count } of this.armorSet.setBonuses) {
239
+ if (count < 2 || count > 4)
240
+ continue;
241
+ const val = bonus.stats?.[count]?.[stat] ?? 0;
242
+ if (val > 0) {
243
+ breakdown[bonus.name] = val;
244
+ sum += val;
245
+ }
246
+ }
247
+ // Equipment pieces
248
+ for (const piece of this.armorSet.equipment) {
249
+ if (!piece)
250
+ continue;
251
+ const val = piece.getStat(stat);
252
+ if (val > 0) {
253
+ breakdown[piece.item.name ?? 'Equipment Piece'] = val;
254
+ sum += val;
255
+ }
196
256
  }
197
- // Eqiupment
198
- const equipment = this.armorSet.equipmentFortune;
199
- if (equipment > 0) {
200
- breakdown['Equipment'] = equipment;
201
- sum += equipment;
257
+ // Equipment Set Bonuses
258
+ for (const { bonus, count } of this.armorSet.equipmentSetBonuses) {
259
+ if (count < 2 || count > 4)
260
+ continue;
261
+ const val = bonus.stats?.[count]?.[stat] ?? 0;
262
+ if (val > 0) {
263
+ breakdown[bonus.name] = val;
264
+ sum += val;
265
+ }
202
266
  }
203
267
  // Anita Bonus
204
- const anitaBonus = getFortune(this.options.anitaBonus, ANITA_FORTUNE_UPGRADE);
268
+ const anitaBonus = getFortune(this.options.anitaBonus, ANITA_FORTUNE_UPGRADE, stat);
205
269
  if (anitaBonus > 0) {
206
270
  breakdown['Anita Bonus Drops'] = anitaBonus;
207
271
  sum += anitaBonus;
208
272
  }
209
273
  // Community Center
210
- const communityCenter = getFortune(this.options.communityCenter, COMMUNITY_CENTER_UPGRADE);
274
+ const communityCenter = getFortune(this.options.communityCenter, COMMUNITY_CENTER_UPGRADE, stat);
211
275
  if (communityCenter > 0) {
212
276
  breakdown['Community Center'] = communityCenter;
213
277
  sum += communityCenter;
214
278
  }
215
279
  // Selected Pet
216
280
  const pet = this.selectedPet;
217
- if (pet && pet.fortune > 0) {
218
- breakdown[pet.info.name ?? 'Selected Pet'] = pet.fortune;
219
- sum += pet.fortune;
281
+ if (pet) {
282
+ const val = pet.getFortune(stat);
283
+ if (val > 0) {
284
+ breakdown[pet.info.name ?? 'Selected Pet'] = val;
285
+ sum += val;
286
+ }
220
287
  }
221
- // Accessories, only count highest fortune from each family
288
+ // Accessories
222
289
  const families = new Map();
223
- this.activeAccessories = [];
224
- for (const accessory of this.accessories.filter((a) => a.fortune > 0).sort((a, b) => b.fortune - a.fortune)) {
225
- if (!accessory.info.family)
290
+ const activeAccessories = [];
291
+ const sortedAccessories = [...this.accessories]
292
+ .map((a) => ({ acc: a, val: a.getStat(stat) }))
293
+ .filter((item) => item.val > 0)
294
+ .sort((a, b) => b.val - a.val);
295
+ for (const { acc, val } of sortedAccessories) {
296
+ if (!acc.info.family)
226
297
  continue;
227
- const existing = families.get(accessory.info.family);
298
+ const existing = families.get(acc.info.family);
228
299
  if (!existing) {
229
- families.set(accessory.info.family, accessory);
230
- this.activeAccessories.push(accessory);
231
- }
232
- else if (accessory.info.familyOrder && accessory.info.familyOrder > (existing.info.familyOrder ?? 0)) {
233
- families.set(accessory.info.family, accessory);
234
- this.activeAccessories.push(accessory);
235
- this.activeAccessories = this.activeAccessories.filter((a) => a !== existing);
300
+ families.set(acc.info.family, acc);
301
+ activeAccessories.push(acc);
302
+ breakdown[acc.item.name ?? acc.item.skyblockId ?? 'Accessory'] = val;
303
+ sum += val;
236
304
  }
237
305
  }
238
- for (const accessory of this.activeAccessories) {
239
- if (accessory.info.crops)
240
- continue;
241
- breakdown[accessory.item.name ?? accessory.item.skyblockId ?? 'Accessory [Error]'] = accessory.fortune;
242
- sum += accessory.fortune;
306
+ if (stat === Stat.FarmingFortune) {
307
+ this.activeAccessories = activeAccessories;
243
308
  }
244
309
  // Refined Truffles
245
- const truffles = Math.min(5, this.options.refinedTruffles ?? 0);
246
- if (truffles > 0) {
247
- breakdown['Refined Truffles'] = truffles;
248
- sum += truffles;
310
+ if (stat === Stat.FarmingFortune) {
311
+ const truffles = Math.min(5, this.options.refinedTruffles ?? 0);
312
+ if (truffles > 0) {
313
+ breakdown['Refined Truffles'] = truffles;
314
+ sum += truffles;
315
+ }
316
+ }
317
+ // Wriggling Larva
318
+ if (stat === Stat.BonusPestChance) {
319
+ const used = Math.min(5, this.options.wrigglingLarva ?? 0);
320
+ const val = getFortune(used, WRIGGLING_LARVA_SOURCE, stat);
321
+ if (val > 0) {
322
+ breakdown['Wriggling Larva'] = val;
323
+ sum += val;
324
+ }
249
325
  }
250
326
  // Attribute Shards
251
327
  for (const [shardId, value] of Object.entries(this.attributes)) {
252
328
  const shard = FARMING_ATTRIBUTE_SHARDS[shardId];
253
329
  if (!shard || value <= 0)
254
330
  continue;
255
- const fortune = getShardFortune(shard, this);
256
- if (fortune <= 0)
331
+ const val = getShardStat(shard, this, stat);
332
+ if (val <= 0)
257
333
  continue;
258
- breakdown[shard.name] = fortune;
259
- sum += fortune;
334
+ breakdown[shard.name] = val;
335
+ sum += val;
260
336
  }
261
337
  // Extra Fortune
262
- for (const extra of this.options.extraFortune ?? []) {
263
- if (extra.crop)
264
- continue;
265
- breakdown[extra.name ?? 'Extra Fortune'] = extra.fortune;
266
- sum += extra.fortune;
267
- }
268
- const temp = this.getTempFortune();
269
- if (temp > 0) {
270
- breakdown['Temporary Fortune'] = temp;
338
+ if (stat === Stat.FarmingFortune) {
339
+ for (const extra of this.options.extraFortune ?? []) {
340
+ if (extra.crop)
341
+ continue;
342
+ breakdown[extra.name ?? 'Extra Fortune'] = extra.fortune;
343
+ sum += extra.fortune;
344
+ }
345
+ const temp = this.getTempFortune();
346
+ if (temp > 0) {
347
+ breakdown['Temporary Fortune'] = temp;
348
+ }
271
349
  }
272
- this.breakdown = breakdown;
273
- return sum;
350
+ return { value: sum, breakdown };
274
351
  }
275
352
  getTempFortune() {
276
353
  let sum = 0;
@@ -360,8 +437,8 @@ export class FarmingPlayer {
360
437
  breakdown: breakdown,
361
438
  };
362
439
  }
363
- getCropProgress(crop) {
364
- return getSourceProgress({ crop, player: this }, CROP_FORTUNE_SOURCES);
440
+ getCropProgress(crop, stats) {
441
+ return getSourceProgress({ crop, player: this }, CROP_FORTUNE_SOURCES, false, stats);
365
442
  }
366
443
  getRates(crop, blocksBroken) {
367
444
  const tool = this.getBestTool(crop);
@@ -390,5 +467,554 @@ export class FarmingPlayer {
390
467
  getSelectedCropTool(crop) {
391
468
  return this.selectedTool?.crop === crop ? this.selectedTool : this.getBestTool(crop);
392
469
  }
470
+ applyUpgrade(upgrade) {
471
+ if (!upgrade.meta)
472
+ return;
473
+ const { type, itemUuid, key, value, id } = upgrade.meta;
474
+ if (itemUuid) {
475
+ const candidates = [...this.tools, ...this.armor, ...this.equipment, ...this.accessories];
476
+ const target = candidates.find((i) => i.item.uuid === itemUuid);
477
+ if (target) {
478
+ if (type === 'enchant' && key) {
479
+ target.item.enchantments ??= {};
480
+ target.item.enchantments[key] = Number(value);
481
+ // Re-instantiate to update enchantment-based logic
482
+ if (target instanceof FarmingTool) {
483
+ const idx = this.tools.indexOf(target);
484
+ if (idx >= 0) {
485
+ this.tools[idx] = new FarmingTool(target.item, this.options);
486
+ }
487
+ }
488
+ else if (target instanceof FarmingArmor) {
489
+ const idx = this.armor.indexOf(target);
490
+ if (idx >= 0) {
491
+ const updatedPiece = new FarmingArmor(target.item, this.options);
492
+ this.armor[idx] = updatedPiece;
493
+ this.armorSet.updateArmorSlot(updatedPiece);
494
+ }
495
+ }
496
+ else if (target instanceof FarmingEquipment) {
497
+ const idx = this.equipment.indexOf(target);
498
+ if (idx >= 0) {
499
+ const updatedPiece = new FarmingEquipment(target.item, this.options);
500
+ this.equipment[idx] = updatedPiece;
501
+ this.armorSet.updateEquipmentSlot(updatedPiece);
502
+ }
503
+ }
504
+ else if (target instanceof FarmingAccessory) {
505
+ const idx = this.accessories.indexOf(target);
506
+ if (idx >= 0) {
507
+ this.accessories[idx] = new FarmingAccessory(target.item, this.options);
508
+ }
509
+ }
510
+ }
511
+ else if (type === 'reforge' && id) {
512
+ target.item.attributes ??= {};
513
+ target.item.attributes.modifier = id;
514
+ // Re-initialize to update logic
515
+ if (target instanceof FarmingTool) {
516
+ const idx = this.tools.indexOf(target);
517
+ if (idx >= 0) {
518
+ this.tools[idx] = new FarmingTool(target.item, this.options);
519
+ }
520
+ }
521
+ else if (target instanceof FarmingArmor) {
522
+ const idx = this.armor.indexOf(target);
523
+ if (idx >= 0) {
524
+ const updatedPiece = new FarmingArmor(target.item, this.options);
525
+ this.armor[idx] = updatedPiece;
526
+ this.armorSet.updateArmorSlot(updatedPiece);
527
+ }
528
+ }
529
+ else if (target instanceof FarmingEquipment) {
530
+ const idx = this.equipment.indexOf(target);
531
+ if (idx >= 0) {
532
+ const updatedPiece = new FarmingEquipment(target.item, this.options);
533
+ this.equipment[idx] = updatedPiece;
534
+ this.armorSet.updateEquipmentSlot(updatedPiece);
535
+ }
536
+ }
537
+ else if (target instanceof FarmingAccessory) {
538
+ const idx = this.accessories.indexOf(target);
539
+ if (idx >= 0) {
540
+ this.accessories[idx] = new FarmingAccessory(target.item, this.options);
541
+ }
542
+ }
543
+ }
544
+ else if (type === 'item' && id === 'farming_for_dummies_count') {
545
+ target.item.attributes ??= {};
546
+ target.item.attributes.farming_for_dummies_count = String(value);
547
+ // Re-instantiate so getUpgrades reflects the updated FFD count
548
+ if (target instanceof FarmingTool) {
549
+ const idx = this.tools.indexOf(target);
550
+ if (idx >= 0) {
551
+ this.tools[idx] = new FarmingTool(target.item, this.options);
552
+ }
553
+ }
554
+ }
555
+ else if (type === 'gem' && upgrade.meta.slot && value) {
556
+ target.item.gems ??= {};
557
+ target.item.gems[upgrade.meta.slot] = String(value);
558
+ // Re-instantiate so getUpgrades reflects the updated gem
559
+ if (target instanceof FarmingTool) {
560
+ const idx = this.tools.indexOf(target);
561
+ if (idx >= 0) {
562
+ this.tools[idx] = new FarmingTool(target.item, this.options);
563
+ }
564
+ }
565
+ else if (target instanceof FarmingArmor) {
566
+ const idx = this.armor.indexOf(target);
567
+ if (idx >= 0) {
568
+ const updatedPiece = new FarmingArmor(target.item, this.options);
569
+ this.armor[idx] = updatedPiece;
570
+ this.armorSet.updateArmorSlot(updatedPiece);
571
+ }
572
+ }
573
+ else if (target instanceof FarmingEquipment) {
574
+ const idx = this.equipment.indexOf(target);
575
+ if (idx >= 0) {
576
+ const updatedPiece = new FarmingEquipment(target.item, this.options);
577
+ this.equipment[idx] = updatedPiece;
578
+ this.armorSet.updateEquipmentSlot(updatedPiece);
579
+ }
580
+ }
581
+ else if (target instanceof FarmingAccessory) {
582
+ const idx = this.accessories.indexOf(target);
583
+ if (idx >= 0) {
584
+ this.accessories[idx] = new FarmingAccessory(target.item, this.options);
585
+ }
586
+ }
587
+ }
588
+ else if (type === 'item' && id === 'rarity_upgrades' && value) {
589
+ target.item.attributes ??= {};
590
+ target.item.attributes.rarity_upgrades = String(value);
591
+ // Recomb affects rarity, which affects stats. Need to reload tool.
592
+ if (target instanceof FarmingTool) {
593
+ const idx = this.tools.indexOf(target);
594
+ if (idx >= 0) {
595
+ this.tools[idx] = new FarmingTool(target.item, this.options);
596
+ }
597
+ }
598
+ else if (target instanceof FarmingArmor) {
599
+ const idx = this.armor.indexOf(target);
600
+ if (idx >= 0) {
601
+ const updatedPiece = new FarmingArmor(target.item, this.options);
602
+ this.armor[idx] = updatedPiece;
603
+ this.armorSet.updateArmorSlot(updatedPiece);
604
+ }
605
+ }
606
+ else if (target instanceof FarmingEquipment) {
607
+ const idx = this.equipment.indexOf(target);
608
+ if (idx >= 0) {
609
+ const updatedPiece = new FarmingEquipment(target.item, this.options);
610
+ this.equipment[idx] = updatedPiece;
611
+ this.armorSet.updateEquipmentSlot(updatedPiece);
612
+ }
613
+ }
614
+ else if (target instanceof FarmingAccessory) {
615
+ const idx = this.accessories.indexOf(target);
616
+ if (idx >= 0) {
617
+ this.accessories[idx] = new FarmingAccessory(target.item, this.options);
618
+ }
619
+ }
620
+ }
621
+ else if (type === 'buy_item' && id) {
622
+ // Tier upgrade: replace the old item with a new one
623
+ const newItem = getFakeItem(id);
624
+ if (newItem) {
625
+ // Transfer enchantments, attributes, gems from old item
626
+ newItem.item.enchantments = {
627
+ ...newItem.item.enchantments,
628
+ ...target.item.enchantments,
629
+ };
630
+ newItem.item.attributes = {
631
+ ...newItem.item.attributes,
632
+ ...target.item.attributes,
633
+ };
634
+ newItem.item.gems = {
635
+ ...newItem.item.gems,
636
+ ...target.item.gems,
637
+ };
638
+ // Preserve the old item's UUID so the item remains trackable
639
+ newItem.item.uuid = target.item.uuid;
640
+ if (target instanceof FarmingTool && newItem instanceof FarmingTool) {
641
+ const idx = this.tools.indexOf(target);
642
+ if (idx >= 0) {
643
+ this.tools[idx] = new FarmingTool(newItem.item, this.options);
644
+ }
645
+ }
646
+ else if (target instanceof FarmingArmor && newItem instanceof FarmingArmor) {
647
+ const idx = this.armor.indexOf(target);
648
+ if (idx >= 0) {
649
+ const updatedPiece = new FarmingArmor(newItem.item, this.options);
650
+ this.armor[idx] = updatedPiece;
651
+ this.armorSet.updateArmorSlot(updatedPiece);
652
+ }
653
+ }
654
+ else if (target instanceof FarmingEquipment && newItem instanceof FarmingEquipment) {
655
+ const idx = this.equipment.indexOf(target);
656
+ if (idx >= 0) {
657
+ const updatedPiece = new FarmingEquipment(newItem.item, this.options);
658
+ this.equipment[idx] = updatedPiece;
659
+ this.armorSet.updateEquipmentSlot(updatedPiece);
660
+ }
661
+ }
662
+ else if (target instanceof FarmingAccessory && newItem instanceof FarmingAccessory) {
663
+ const idx = this.accessories.indexOf(target);
664
+ if (idx >= 0) {
665
+ this.accessories[idx] = new FarmingAccessory(newItem.item, this.options);
666
+ }
667
+ }
668
+ this.permFortune = this.getGeneralFortune();
669
+ }
670
+ }
671
+ }
672
+ }
673
+ else if (type === 'skill') {
674
+ if (key === 'farmingLevel' && value) {
675
+ this.options.farmingLevel = Number(value);
676
+ }
677
+ else if (key === 'anitaBonus' && value) {
678
+ this.options.anitaBonus = Number(value);
679
+ }
680
+ else if (key === 'communityCenter' && value) {
681
+ this.options.communityCenter = Number(value);
682
+ }
683
+ this.permFortune = this.getGeneralFortune();
684
+ }
685
+ else if (type === 'plot' && (value || id)) {
686
+ this.options.plotsUnlocked = Number(value);
687
+ // Also add to plots array if using id (the plot name)
688
+ if (id) {
689
+ this.options.plots ??= [];
690
+ if (!this.options.plots.includes(id)) {
691
+ this.options.plots.push(id);
692
+ }
693
+ }
694
+ this.permFortune = this.getGeneralFortune();
695
+ }
696
+ else if (type === 'attribute' && key && value) {
697
+ this.options.attributes ??= {};
698
+ this.options.attributes[key] = Number(value);
699
+ this.permFortune = this.getGeneralFortune();
700
+ }
701
+ else if (type === 'crop_upgrade' && key && value) {
702
+ this.options.cropUpgrades ??= {};
703
+ // @ts-ignore
704
+ this.options.cropUpgrades[key] = Number(value);
705
+ this.permFortune = this.getGeneralFortune();
706
+ }
707
+ else if (type === 'setting' && key && value) {
708
+ if (key === 'cocoaFortuneUpgrade') {
709
+ this.options.cocoaFortuneUpgrade = Number(value);
710
+ }
711
+ this.permFortune = this.getGeneralFortune();
712
+ }
713
+ else if (type === 'unlock' && id) {
714
+ if (id === 'personal_best') {
715
+ this.options.personalBestsUnlocked = true;
716
+ }
717
+ this.permFortune = this.getGeneralFortune();
718
+ }
719
+ else if (type === 'buy_item' && id) {
720
+ const newItem = getFakeItem(id);
721
+ if (newItem) {
722
+ if (newItem instanceof FarmingTool) {
723
+ const oldIdx = itemUuid ? this.tools.findIndex((t) => t.item.uuid === itemUuid) : -1;
724
+ if (oldIdx >= 0) {
725
+ const oldItem = this.tools[oldIdx];
726
+ // Transfer enchantments, attributes, gems from old item
727
+ newItem.item.enchantments = {
728
+ ...newItem.item.enchantments,
729
+ ...oldItem.item.enchantments,
730
+ };
731
+ newItem.item.attributes = {
732
+ ...newItem.item.attributes,
733
+ ...oldItem.item.attributes,
734
+ };
735
+ newItem.item.gems = { ...newItem.item.gems, ...oldItem.item.gems };
736
+ // Re-instantiate to recalculate fortune with transferred properties
737
+ this.tools[oldIdx] = new FarmingTool(newItem.item, this.options);
738
+ }
739
+ else {
740
+ this.tools.push(newItem);
741
+ }
742
+ }
743
+ else if (newItem instanceof FarmingArmor) {
744
+ const oldIdx = itemUuid ? this.armor.findIndex((a) => a.item.uuid === itemUuid) : -1;
745
+ if (oldIdx >= 0) {
746
+ const oldItem = this.armor[oldIdx];
747
+ newItem.item.enchantments = {
748
+ ...newItem.item.enchantments,
749
+ ...oldItem.item.enchantments,
750
+ };
751
+ newItem.item.attributes = {
752
+ ...newItem.item.attributes,
753
+ ...oldItem.item.attributes,
754
+ };
755
+ newItem.item.gems = { ...newItem.item.gems, ...oldItem.item.gems };
756
+ this.armor[oldIdx] = new FarmingArmor(newItem.item, this.options);
757
+ }
758
+ else {
759
+ this.armor.push(newItem);
760
+ }
761
+ }
762
+ else if (newItem instanceof FarmingEquipment) {
763
+ const oldIdx = itemUuid ? this.equipment.findIndex((e) => e.item.uuid === itemUuid) : -1;
764
+ if (oldIdx >= 0) {
765
+ const oldItem = this.equipment[oldIdx];
766
+ newItem.item.enchantments = {
767
+ ...newItem.item.enchantments,
768
+ ...oldItem.item.enchantments,
769
+ };
770
+ newItem.item.attributes = {
771
+ ...newItem.item.attributes,
772
+ ...oldItem.item.attributes,
773
+ };
774
+ newItem.item.gems = { ...newItem.item.gems, ...oldItem.item.gems };
775
+ this.equipment[oldIdx] = new FarmingEquipment(newItem.item, this.options);
776
+ }
777
+ else {
778
+ this.equipment.push(newItem);
779
+ }
780
+ }
781
+ else if (newItem instanceof FarmingAccessory) {
782
+ const oldIdx = itemUuid ? this.accessories.findIndex((a) => a.item.uuid === itemUuid) : -1;
783
+ if (oldIdx >= 0) {
784
+ const oldItem = this.accessories[oldIdx];
785
+ newItem.item.enchantments = {
786
+ ...newItem.item.enchantments,
787
+ ...oldItem.item.enchantments,
788
+ };
789
+ newItem.item.attributes = {
790
+ ...newItem.item.attributes,
791
+ ...oldItem.item.attributes,
792
+ };
793
+ newItem.item.gems = { ...newItem.item.gems, ...oldItem.item.gems };
794
+ this.accessories[oldIdx] = new FarmingAccessory(newItem.item, this.options);
795
+ }
796
+ else {
797
+ this.accessories.push(newItem);
798
+ }
799
+ }
800
+ this.permFortune = this.getGeneralFortune();
801
+ }
802
+ }
803
+ }
804
+ /**
805
+ * Creates a deep clone of this FarmingPlayer that can be modified without affecting the original.
806
+ */
807
+ clone() {
808
+ const cloneItems = (items) => {
809
+ return items.map((item) => ({
810
+ ...item.item,
811
+ enchantments: { ...item.item.enchantments },
812
+ attributes: { ...item.item.attributes },
813
+ gems: { ...item.item.gems },
814
+ lore: [...(item.item.lore ?? [])],
815
+ }));
816
+ };
817
+ const clonedOptions = {
818
+ ...this.options,
819
+ tools: cloneItems(this.tools),
820
+ armor: cloneItems(this.armor),
821
+ equipment: cloneItems(this.equipment),
822
+ accessories: cloneItems(this.accessories),
823
+ pets: this.pets.map((p) => ({ ...p.pet })),
824
+ cropUpgrades: { ...this.options.cropUpgrades },
825
+ milestones: { ...this.options.milestones },
826
+ exportableCrops: { ...this.options.exportableCrops },
827
+ personalBests: { ...this.options.personalBests },
828
+ collection: { ...this.options.collection },
829
+ bestiaryKills: { ...this.options.bestiaryKills },
830
+ attributes: { ...this.options.attributes },
831
+ plots: [...(this.options.plots ?? [])],
832
+ };
833
+ return new FarmingPlayer(clonedOptions);
834
+ }
835
+ /**
836
+ * Expands an upgrade into a tree of follow-up upgrades.
837
+ * This applies the upgrade on a cloned player and recursively finds upgrades
838
+ * for the same target item.
839
+ *
840
+ * @param upgrade - The upgrade to expand
841
+ * @param options.maxDepth - Maximum recursion depth (default: 10)
842
+ * @param options.crop - Crop for crop-specific fortune calculations
843
+ * @param options.stats - Stats to track (default: [Stat.FarmingFortune])
844
+ * @param options.includeAllTierUpgradeChildren - If true, first-level children of tier upgrades include ALL available upgrades for the new item (default: false)
845
+ */
846
+ expandUpgrade(upgrade, options) {
847
+ const { maxDepth = 10, crop, stats = [Stat.FarmingFortune], includeAllTierUpgradeChildren = false, } = options ?? {};
848
+ const visited = new Set();
849
+ const usedConflictKeys = new Set();
850
+ return this.buildUpgradeTree(upgrade, 0, maxDepth, crop, visited, usedConflictKeys, stats, includeAllTierUpgradeChildren);
851
+ }
852
+ buildUpgradeTree(upgrade, depth, maxDepth, crop, visited, usedConflictKeys, stats, includeAllTierUpgradeChildren) {
853
+ // Create unique key for this upgrade to detect cycles
854
+ const upgradeKey = this.getUpgradeKey(upgrade);
855
+ if (visited.has(upgradeKey)) {
856
+ // Return a leaf node if we've seen this exact upgrade before
857
+ const currentStats = this.getAllStats(stats, crop);
858
+ return {
859
+ upgrade,
860
+ statsBefore: currentStats,
861
+ statsAfter: currentStats,
862
+ statsGained: {},
863
+ totalCost: upgrade.cost,
864
+ children: [],
865
+ };
866
+ }
867
+ visited.add(upgradeKey);
868
+ // Clone player and apply upgrade
869
+ const clonedPlayer = this.clone();
870
+ const statsBefore = clonedPlayer.getAllStats(stats, crop);
871
+ clonedPlayer.applyUpgrade(upgrade);
872
+ const statsAfter = clonedPlayer.getAllStats(stats, crop);
873
+ const statsGained = this.computeStatsDiff(statsBefore, statsAfter);
874
+ const node = {
875
+ upgrade,
876
+ statsBefore,
877
+ statsAfter,
878
+ statsGained,
879
+ totalCost: upgrade.cost,
880
+ children: [],
881
+ };
882
+ // Stop recursion at max depth
883
+ if (depth >= maxDepth) {
884
+ return node;
885
+ }
886
+ // Add current upgrade's conflict key for children to prevent duplicates in siblings
887
+ const childConflictKeys = new Set(usedConflictKeys);
888
+ if (upgrade.conflictKey) {
889
+ childConflictKeys.add(upgrade.conflictKey);
890
+ }
891
+ // For buy_item (tier) upgrades, also add conflict keys from the original item's available upgrades.
892
+ // This prevents duplicate suggestions - if Squash has Pesterminator 1 available,
893
+ // Fermento's children shouldn't also suggest Pesterminator 1 (do it on Squash instead, it carries over).
894
+ // When includeAllTierUpgradeChildren is true and depth is 0, skip this filtering to show ALL upgrades.
895
+ const skipTierFiltering = includeAllTierUpgradeChildren && depth === 0;
896
+ if (!skipTierFiltering && upgrade.meta?.type === 'buy_item' && upgrade.meta?.itemUuid) {
897
+ const originalItem = this.tools.find((t) => t.item.uuid === upgrade.meta?.itemUuid) ??
898
+ this.armor.find((a) => a.item.uuid === upgrade.meta?.itemUuid) ??
899
+ this.equipment.find((e) => e.item.uuid === upgrade.meta?.itemUuid) ??
900
+ this.accessories.find((a) => a.item.uuid === upgrade.meta?.itemUuid);
901
+ if (originalItem && 'getUpgrades' in originalItem && typeof originalItem.getUpgrades === 'function') {
902
+ for (const originalUpgrade of originalItem.getUpgrades()) {
903
+ if (originalUpgrade.conflictKey) {
904
+ childConflictKeys.add(originalUpgrade.conflictKey);
905
+ }
906
+ }
907
+ }
908
+ }
909
+ // Find follow-up upgrades for the same target, excluding those with already-used conflict keys
910
+ const primaryStat = stats[0] ?? Stat.FarmingFortune;
911
+ const followUpUpgrades = this.getFollowUpUpgrades(clonedPlayer, upgrade, crop, primaryStat).filter((u) => !u.conflictKey || !childConflictKeys.has(u.conflictKey));
912
+ // Build children (using the cloned player's state)
913
+ for (const followUp of followUpUpgrades) {
914
+ const childNode = clonedPlayer.buildUpgradeTree(followUp, depth + 1, maxDepth, crop, new Set(visited), childConflictKeys, stats, includeAllTierUpgradeChildren);
915
+ node.children.push(childNode);
916
+ }
917
+ // Sort children by primary stat gained (highest first)
918
+ node.children.sort((a, b) => {
919
+ const aGain = a.statsGained[primaryStat] ?? 0;
920
+ const bGain = b.statsGained[primaryStat] ?? 0;
921
+ return bGain - aGain;
922
+ });
923
+ return node;
924
+ }
925
+ getAllStats(stats, crop) {
926
+ const result = {};
927
+ for (const stat of stats) {
928
+ let value = this.getStat(stat);
929
+ // Add crop-specific fortune if applicable
930
+ if (crop && stat === Stat.FarmingFortune) {
931
+ value += this.getCropFortune(crop).fortune;
932
+ }
933
+ if (value !== 0) {
934
+ result[stat] = value;
935
+ }
936
+ }
937
+ return result;
938
+ }
939
+ computeStatsDiff(before, after) {
940
+ const result = {};
941
+ const allStats = new Set([...Object.keys(before), ...Object.keys(after)]);
942
+ for (const stat of allStats) {
943
+ const beforeVal = before[stat] ?? 0;
944
+ const afterVal = after[stat] ?? 0;
945
+ const diff = afterVal - beforeVal;
946
+ if (diff !== 0) {
947
+ result[stat] = diff;
948
+ }
949
+ }
950
+ return result;
951
+ }
952
+ getUpgradeKey(upgrade) {
953
+ const meta = upgrade.meta;
954
+ if (!meta)
955
+ return `${upgrade.title}`;
956
+ const parts = [meta.type ?? '', meta.id ?? '', meta.itemUuid ?? '', meta.key ?? '', String(meta.value ?? '')];
957
+ return parts.join(':');
958
+ }
959
+ getFollowUpUpgrades(player, appliedUpgrade, crop, primaryStat) {
960
+ const meta = appliedUpgrade.meta;
961
+ if (!meta)
962
+ return [];
963
+ const itemUuid = meta.itemUuid;
964
+ const upgrades = [];
965
+ // Handle buy_item specially - these are item tier upgrades
966
+ // After applying, we want ALL upgrades for the new item, not filtered by type
967
+ // The new item preserves the old UUID, but we search by skyblockId to find the upgraded tier
968
+ if (meta.type === 'buy_item' && meta.id) {
969
+ const newItemId = meta.id;
970
+ const target = player.tools.find((t) => t.item.skyblockId === newItemId) ??
971
+ player.armor.find((a) => a.item.skyblockId === newItemId) ??
972
+ player.equipment.find((e) => e.item.skyblockId === newItemId) ??
973
+ player.accessories.find((a) => a.item.skyblockId === newItemId);
974
+ if (target && 'getUpgrades' in target && typeof target.getUpgrades === 'function') {
975
+ upgrades.push(...target.getUpgrades({ stat: primaryStat }));
976
+ }
977
+ }
978
+ else if (itemUuid) {
979
+ // Item-specific upgrade - find upgrades for the same item
980
+ const target = player.tools.find((t) => t.item.uuid === itemUuid) ??
981
+ player.armor.find((a) => a.item.uuid === itemUuid) ??
982
+ player.equipment.find((e) => e.item.uuid === itemUuid) ??
983
+ player.accessories.find((a) => a.item.uuid === itemUuid);
984
+ if (target && 'getUpgrades' in target && typeof target.getUpgrades === 'function') {
985
+ const itemUpgrades = target.getUpgrades({ stat: primaryStat });
986
+ // Filter to only include upgrades of the same type (enchant chains, tier upgrades, etc.)
987
+ // For gem upgrades, also match on slot to only show follow-ups for that specific slot
988
+ for (const u of itemUpgrades) {
989
+ if (u.meta?.type === meta.type && u.meta?.key === meta.key) {
990
+ // For gem upgrades, also require matching slot
991
+ if (meta.type === 'gem' && meta.slot && u.meta?.slot !== meta.slot) {
992
+ continue;
993
+ }
994
+ upgrades.push(u);
995
+ }
996
+ }
997
+ }
998
+ }
999
+ else if (meta.type === 'skill' || meta.type === 'plot' || meta.type === 'attribute') {
1000
+ // General upgrades - find the next level of the same upgrade type
1001
+ const generalUpgrades = player.getUpgrades({ stat: primaryStat });
1002
+ for (const u of generalUpgrades) {
1003
+ if (u.meta?.type === meta.type && u.meta?.key === meta.key) {
1004
+ upgrades.push(u);
1005
+ }
1006
+ }
1007
+ }
1008
+ else if (meta.type === 'crop_upgrade' && crop) {
1009
+ // Crop-specific upgrades
1010
+ const cropUpgrades = player.getCropUpgrades(crop);
1011
+ for (const u of cropUpgrades) {
1012
+ if (u.meta?.type === meta.type && u.meta?.key === meta.key) {
1013
+ upgrades.push(u);
1014
+ }
1015
+ }
1016
+ }
1017
+ return upgrades;
1018
+ }
393
1019
  }
394
1020
  //# sourceMappingURL=player.js.map