farming-weight
Version:
Tools for calculating farming weight and fortune in Hypixel Skyblock
608 lines • 25.4 kB
JavaScript
import { compareRarity, REFORGES } from '../constants/reforges.js';
import { Stat } from '../constants/stats.js';
import { getQueryStats, UpgradeAction, UpgradeCategory, UpgradeReason, } from '../constants/upgrades.js';
import { effectsToSummaries } from '../effects/summary.js';
import { GemRarity } from '../fortune/item.js';
import { getLotusToBlossomPieceBonus } from '../fortune/lotuspiecebonus.js';
import { gemSlotDeltaEffectSummaries } from '../items/sources/gems.js';
import { reforgeEffects } from '../items/sources/reforges.js';
import { FARMING_TOOLS } from '../items/tools.js';
import { getGemRarityName, getNextGemRarity, getPeridotFortune, getPeridotGemFortune } from '../util/gems.js';
import { nextRarity, previousRarity } from '../util/itemstats.js';
import { getUpgradeableEnchants } from './enchantupgrades.js';
import { getItemInfo } from './itemcatalog.js';
import { getItemScopedConflictKey } from './upgradekeys.js';
function getPieceBonus(upgradeable) {
const fn = upgradeable.getPieceBonus;
return typeof fn === 'function' ? fn.call(upgradeable) : 0;
}
function getBaseStats(info, options) {
const stats = { ...(info.baseStats ?? {}) };
const computed = info.computedStats?.(options ?? {}) ?? {};
for (const stat of Object.values(Stat)) {
const value = computed[stat];
if (value !== undefined) {
stats[stat] = (stats[stat] ?? 0) + value;
}
}
return stats;
}
function getFakeUpgradeableOfSameKind(upgradeable, info) {
if (!info)
return undefined;
const c = upgradeable.constructor;
return c.fakeItem?.(info, upgradeable.options);
}
function createInfoFake(info) {
if (!info)
return undefined;
const getStat = (stat) => getBaseStats(info)[stat] ?? 0;
const item = {
name: info.name,
skyblockId: info.skyblockId,
attributes: {},
enchantments: {},
};
const fake = {
item,
info,
options: undefined,
recombobulated: false,
rarity: info.maxRarity,
reforge: undefined,
reforgeStats: undefined,
fortune: 0,
getFortune() {
return getStat(Stat.FarmingFortune);
},
getStat(stat) {
return getStat(stat);
},
getStats() {
const result = {};
for (const stat of Object.values(Stat)) {
const value = getStat(stat);
if (value > 0)
result[stat] = value;
}
return result;
},
getUpgrades() {
return [];
},
getItemUpgrade() {
return info.upgrade;
},
getLastItemUpgrade() {
return undefined;
},
getProgress() {
return [];
},
};
fake.fortune = fake.getFortune();
return fake;
}
function getRequestedStats(stats) {
return stats && stats.length > 0 ? stats : [Stat.FarmingFortune];
}
export function getReforgeEffectSummaries(upgradeable, reforgeId, stats, sourceName) {
return effectsToSummaries(reforgeEffects(reforgeId, upgradeable.rarity, sourceName), getRequestedStats(stats));
}
export function getCurrentReforgeEffectSummaries(upgradeable, stats) {
const reforgeId = upgradeable.item.attributes?.modifier;
if (!reforgeId)
return [];
return getReforgeEffectSummaries(upgradeable, reforgeId, stats);
}
export function getItemUpgrades(upgradeable, options) {
const stats = getQueryStats(options);
const { deadEnd, upgrade } = getSelfFortuneUpgrade(upgradeable, options) ?? {};
if (deadEnd)
return [upgrade];
const upgrades = [];
upgrades.push(upgrade);
upgrades.push(getUpgradeableRarityUpgrade(upgradeable));
for (const stat of stats) {
upgrades.push(...getUpgradeableEnchants(upgradeable, stat));
}
upgrades.push(...getUpgradeableGems(upgradeable));
upgrades.push(...getUpgradeableReforges(upgradeable, stats));
return upgrades.filter((u) => u);
}
export function getSelfFortuneUpgrade(upgradeable, options) {
const nextItem = upgradeable.getItemUpgrade();
const deadEnd = nextItem && nextItem.reason == UpgradeReason.DeadEnd;
const { info: nextInfo } = getUpgradeableInfo(nextItem?.id);
const nextFake = getFakeUpgradeableOfSameKind(upgradeable, nextInfo);
const slot = upgradeable.info.slot;
if (deadEnd && nextInfo) {
const nextStats = nextFake?.getStats() ?? getBaseStats(nextInfo, upgradeable.options);
const currentStats = upgradeable.getStats();
const deltaStats = {};
for (const stat of Object.values(Stat)) {
const delta = (nextStats[stat] ?? 0) - (currentStats[stat] ?? 0);
if (delta !== 0)
deltaStats[stat] = delta;
}
return {
deadEnd: true,
upgrade: {
title: nextInfo.name,
increase: nextFake?.getFortune() ?? deltaStats[Stat.FarmingFortune] ?? 0,
stats: deltaStats,
wiki: nextInfo.wiki,
action: UpgradeAction.Purchase,
purchase: nextInfo.skyblockId,
category: UpgradeCategory.Item,
cost: {
...(nextItem.cost ?? {
items: {
[nextInfo.skyblockId]: 1,
},
}),
},
skillReq: nextInfo.skillReq,
onto: {
name: upgradeable.item.name,
skyblockId: upgradeable.item.skyblockId,
newSkyblockId: nextInfo.skyblockId,
slot,
},
meta: {
type: 'buy_item',
id: nextInfo.skyblockId,
},
group: nextItem.group,
conflictKey: getItemScopedConflictKey(upgradeable, `item_tier:${nextInfo.skyblockId}`),
},
};
}
else if (nextItem && nextInfo && !(nextItem.reason === UpgradeReason.Situational && !nextItem.preferred)) {
// For item upgrades, compare base stats only (not total fortune which includes reforge/enchants)
// This ensures the delta accurately represents what changes when upgrading the item itself
const currentBaseStats = getBaseStats(upgradeable.info, upgradeable.options);
const nextBaseStats = getBaseStats(nextInfo, upgradeable.options);
const deltaStats = {};
for (const stat of Object.values(Stat)) {
const delta = (nextBaseStats[stat] ?? 0) - (currentBaseStats[stat] ?? 0);
if (delta !== 0)
deltaStats[stat] = delta;
}
let increase = deltaStats[Stat.FarmingFortune] ?? 0;
const stats = getQueryStats(options);
for (const stat of stats) {
if (stat !== Stat.FarmingFortune)
increase += deltaStats[stat] ?? 0;
}
const currentPieceBonus = getPieceBonus(upgradeable);
const upgradedPieceBonus = getLotusToBlossomPieceBonus(upgradeable.item.skyblockId, nextInfo.skyblockId, currentPieceBonus);
if (upgradedPieceBonus !== undefined) {
increase += upgradedPieceBonus - currentPieceBonus;
deltaStats[Stat.FarmingFortune] = increase;
}
// Account for gem rarity changes when the item's base rarity increases
// This applies to any item with gems (armor, equipment, etc.)
if (upgradeable.item.gems) {
const currentRarity = upgradeable.rarity;
const currentBaseRarity = previousRarity(upgradeable.info.maxRarity) ?? upgradeable.info.maxRarity;
const nextBaseRarity = previousRarity(nextInfo.maxRarity) ?? nextInfo.maxRarity;
// Determine what rarity the upgraded item will be
const rarityIncrease = compareRarity(currentRarity, currentBaseRarity);
let nextItemRarity = nextBaseRarity;
if (rarityIncrease > 0) {
// Current item is recombobulated, so next item will also be recombobulated
nextItemRarity = nextRarity(nextBaseRarity);
}
if (compareRarity(nextItemRarity, currentRarity) > 0) {
const currentGemFortune = getPeridotFortune(currentRarity, upgradeable.item);
const nextGemFortune = getPeridotFortune(nextItemRarity, upgradeable.item);
increase += nextGemFortune - currentGemFortune;
deltaStats[Stat.FarmingFortune] = increase;
}
}
// Account for reforge stat changes when the item's base rarity increases
// Reforge stats scale with item rarity (e.g., Bustling gives more fortune on Legendary vs Epic)
if (upgradeable.reforge) {
const currentRarity = upgradeable.rarity;
const currentBaseRarity = previousRarity(upgradeable.info.maxRarity) ?? upgradeable.info.maxRarity;
const nextBaseRarity = previousRarity(nextInfo.maxRarity) ?? nextInfo.maxRarity;
// Determine what rarity the upgraded item will be
const rarityIncrease = compareRarity(currentRarity, currentBaseRarity);
let nextItemRarity = nextBaseRarity;
if (rarityIncrease > 0) {
// Current item is recombobulated, so next item will also be recombobulated
nextItemRarity = nextRarity(nextBaseRarity);
}
if (compareRarity(nextItemRarity, currentRarity) > 0) {
const currentReforgeFortune = upgradeable.reforge.tiers?.[currentRarity]?.stats?.[Stat.FarmingFortune] ?? 0;
const nextReforgeFortune = upgradeable.reforge.tiers?.[nextItemRarity]?.stats?.[Stat.FarmingFortune] ?? 0;
increase += nextReforgeFortune - currentReforgeFortune;
deltaStats[Stat.FarmingFortune] = increase;
}
}
return {
deadEnd: false,
upgrade: {
title: nextInfo.name,
increase,
stats: deltaStats,
wiki: nextInfo.wiki,
action: nextItem.reason === UpgradeReason.Situational ? UpgradeAction.Purchase : UpgradeAction.Upgrade,
purchase: nextItem.reason === UpgradeReason.Situational ? nextItem.id : undefined,
category: UpgradeCategory.Item,
cost: {
...(nextItem.cost ?? {
items: {
[nextItem.id]: 1,
},
}),
},
skillReq: nextInfo.skillReq,
onto: {
name: upgradeable.item.name,
skyblockId: upgradeable.item.skyblockId,
newSkyblockId: nextInfo.skyblockId,
slot,
},
meta: {
type: 'buy_item',
id: nextItem.id,
itemUuid: nextItem.reason === UpgradeReason.Situational
? undefined
: (upgradeable.item.uuid ?? undefined),
},
group: nextItem.group,
conflictKey: getItemScopedConflictKey(upgradeable, `item_tier:${nextItem.id}`),
},
};
}
}
export function getLastToolUpgrade(tool) {
const upgrade = tool.upgrade;
if (!upgrade)
return undefined;
let last = upgrade;
let item = FARMING_TOOLS[upgrade.id];
if (!item)
return undefined;
while (item?.upgrade && (item.upgrade.reason !== UpgradeReason.Situational || item.upgrade.preferred)) {
last = item.upgrade;
item = FARMING_TOOLS[item.upgrade.id];
}
if (!item || last === upgrade)
return undefined;
return item;
}
export function getUpgradeableInfo(skyblockId) {
const info = getItemInfo(skyblockId);
return { info, fake: createInfoFake(info) };
}
export function getNextItemUpgradeableTo(upgradeable, options) {
const upgrade = upgradeable.getItemUpgrade();
if (!upgrade)
return undefined;
const next = options[upgrade.id];
if (!next)
return undefined;
return { upgrade: upgrade, info: next };
}
export function getLastItemUpgradeableTo(upgradeable, options) {
const upgrade = upgradeable.getItemUpgrade();
if (!upgrade || (upgrade.reason === UpgradeReason.Situational && !upgrade.preferred))
return undefined;
let last = upgrade;
let item = options[upgrade.id];
if (!item)
return undefined;
while (item?.upgrade && (item.upgrade.reason !== UpgradeReason.Situational || item.upgrade.preferred)) {
last = item.upgrade;
item = options[item.upgrade.id];
}
if (!item)
return undefined;
return { upgrade: last, info: item };
}
export function getUpgradeableRarityUpgrade(upgradeable) {
// Skip if the item is already recombobulated
if (upgradeable.recombobulated)
return;
const rarity = upgradeable.rarity;
const next = nextRarity(upgradeable.rarity);
const result = {
title: 'Recombobulate ' + upgradeable.item.name,
increase: 0,
stats: {
[Stat.FarmingFortune]: 0,
},
action: UpgradeAction.Recombobulate,
category: UpgradeCategory.Rarity,
improvements: [],
cost: {
items: {
RECOMBOBULATOR_3000: 1,
},
},
onto: {
name: upgradeable.item.name,
skyblockId: upgradeable.item.skyblockId,
},
meta: {
itemUuid: upgradeable.item.uuid ?? undefined,
type: 'item',
id: 'rarity_upgrades',
value: 1,
},
conflictKey: getItemScopedConflictKey(upgradeable, 'recombobulate'),
};
// Gemstone fortune increases with rarity
// Calculate the difference in fortune between the current and next rarity
const currentPeridot = getPeridotFortune(upgradeable.rarity, upgradeable.item);
const nextPeridot = getPeridotFortune(next, upgradeable.item);
if (nextPeridot > currentPeridot) {
result.increase += nextPeridot - currentPeridot;
result.improvements.push({
name: 'Peridot Gems Rarity Increase',
fortune: nextPeridot - currentPeridot,
});
}
// Reforge fortune increases with rarity
// Calculate the difference in fortune between the current and next rarity
if (!upgradeable.reforge) {
if (result.increase <= 0)
return undefined;
return result;
}
const reforgeTiers = upgradeable.reforge.tiers;
const currentFortune = reforgeTiers?.[rarity]?.stats?.[Stat.FarmingFortune] ?? 0;
const nextFortune = reforgeTiers?.[next]?.stats?.[Stat.FarmingFortune] ?? 0;
if (nextFortune > currentFortune) {
result.increase += nextFortune - currentFortune;
result.improvements.push({
name: 'Reforge Rarity Increase',
fortune: nextFortune - currentFortune,
});
}
if (result.increase <= 0)
return undefined;
return result;
}
export function getUpgradeableReforges(upgradeable, stats) {
const requestedStats = getRequestedStats(stats);
const primaryStat = requestedStats[0] ?? Stat.FarmingFortune;
const currentPrimary = upgradeable.reforgeStats?.stats?.[primaryStat] ?? 0;
const currentStats = upgradeable.reforgeStats?.stats ?? {};
const result = [];
for (const [reforgeId, reforge] of Object.entries(REFORGES)) {
// Skip if the reforge doesn't apply to the item or is currently applied
if (!upgradeable.type ||
!reforge ||
!reforge.appliesTo.includes(upgradeable.type) ||
reforge === upgradeable.reforge) {
continue;
}
// Only suggest reforges with an explicit reforge stone (keeps output consistent and costable)
if (!reforge.stone?.id)
continue;
const tier = reforge.tiers[upgradeable.rarity];
if (!tier || !tier.stats)
continue;
const effects = getReforgeEffectSummaries(upgradeable, reforgeId, requestedStats);
const nextPrimary = tier.stats?.[primaryStat] ?? 0;
// Skip if the reforge doesn't improve the selected stat
if (nextPrimary <= currentPrimary &&
!requestedStats.some((stat) => (tier.stats?.[stat] ?? 0) > (currentStats?.[stat] ?? 0)) &&
effects.length === 0) {
continue;
}
const deltaStats = {};
for (const stat of Object.values(Stat)) {
const before = currentStats?.[stat] ?? 0;
const after = tier.stats?.[stat] ?? 0;
const diff = after - before;
if (diff !== 0)
deltaStats[stat] = diff;
}
const increase = deltaStats[Stat.FarmingFortune] ?? 0;
result.push({
title: 'Reforge to ' + reforge.name,
increase,
stats: deltaStats,
effects: effects.length > 0 ? effects : undefined,
action: UpgradeAction.Apply,
category: UpgradeCategory.Reforge,
conflictKey: getItemScopedConflictKey(upgradeable, 'reforge'),
wiki: reforge.wiki,
onto: {
name: upgradeable.item.name,
skyblockId: upgradeable.item.skyblockId,
},
cost: reforge.stone?.id
? {
items: {
[reforge.stone.id]: 1,
},
applyCost: tier.cost
? {
coins: tier.cost,
}
: undefined,
}
: undefined,
meta: {
type: 'reforge',
id: reforge.name.toLowerCase().replaceAll(' ', '_'),
itemUuid: upgradeable.item.uuid ?? undefined,
},
});
}
return result;
}
export function getUpgradeableGems(upgradeable) {
const peridotSlots = upgradeable.info.gemSlots?.filter((s) => s.slot_type === 'PERIDOT');
if (!peridotSlots || peridotSlots.length < 1)
return [];
const result = [];
const multiplier = getGemStatMultiplier(upgradeable);
const gemSourceName = `${upgradeable.item.name} (Gems)`;
// Add entries for applying missing gems
for (let i = 0; i < peridotSlots.length; i++) {
const slot = peridotSlots[i];
if (!slot)
continue;
if (!meetsGemSlotRequirements(upgradeable, slot))
continue;
const slotId = slot.slot_type + '_' + i;
// Check that the slot is not unlocked
if (upgradeable.item?.gems?.[slotId] !== undefined)
continue;
// Intentionally skipping Rough and Flawed gems as they are not really worth applying
// A way to configure this would be nice at some point
const cost = {
items: {
FINE_PERIDOT_GEM: 1,
},
};
if (slot.costs) {
for (const costItem of slot.costs) {
if (costItem.type === 'ITEM') {
cost.items ??= {};
cost.items[costItem.item_id] = costItem.amount + (cost.items[costItem.item_id] ?? 0);
}
else if (costItem.type === 'COINS') {
cost.coins = (cost.coins ?? 0) + (costItem.coins ?? 0);
}
}
}
const increase = +(getPeridotGemFortune(upgradeable.rarity, GemRarity.Fine) * multiplier).toFixed(2);
const effects = gemSlotDeltaEffectSummaries(slotId, upgradeable.rarity, null, GemRarity.Fine, gemSourceName, [Stat.FarmingFortune], multiplier);
result.push({
title: 'Fine Peridot Gemstone',
increase,
stats: {
[Stat.FarmingFortune]: increase,
},
effects: effects.length > 0 ? effects : undefined,
cost: cost,
onto: {
name: upgradeable.item.name,
skyblockId: upgradeable.item.skyblockId,
},
action: UpgradeAction.Apply,
category: UpgradeCategory.Gem,
conflictKey: getItemScopedConflictKey(upgradeable, `gem:${slotId}:${GemRarity.Fine}`),
meta: {
type: 'gem',
slot: slotId,
value: GemRarity.Fine,
itemUuid: upgradeable.item.uuid ?? undefined,
},
});
}
// Add entries for upgrading existing gems
// unlockedSlots is array of GemRarity | null.
// The previous loop handled "applying missing gems" which usually means slot unlocking AND gem placement?
// Wait, getPeridotGems returns the rarities of gems in slots.
// The previous loop checks `upgradeable.item?.gems?.[slotId] !== undefined`.
// If gems are missing, it suggests "Fine Peridot".
// The second loop iterates `unlockedSlots`.
// But we need the SLOT ID for the second loop upgrades too.
// getPeridotGems returns array of values, logic relies on index implicitly mapping to slots?
// getPeridotGems implementation in `src/util/gems.ts` iterates slots PERIDOT_0, PERIDOT_1...
// So `unlockedSlots[i]` corresponds to `PERIDOT_i`.
// Add entries for upgrading existing gems
const gems = upgradeable.item.gems ?? {};
for (const [slotId, gem] of Object.entries(gems)) {
if (!slotId.startsWith('PERIDOT'))
continue;
const gemRarity = gem;
if (gemRarity === undefined)
continue;
if (gemRarity === GemRarity.Perfect)
continue;
// Start at Fine if the gem is null (not applied)
// Flawed and Rough gems are not really worth applying
const nextGem = gemRarity === null ? GemRarity.Fine : getNextGemRarity(gemRarity);
const currentFortune = +(getPeridotGemFortune(upgradeable.rarity, gemRarity) * multiplier).toFixed(2);
const nextFortune = +(getPeridotGemFortune(upgradeable.rarity, nextGem) * multiplier).toFixed(2);
if (nextFortune > currentFortune) {
const effects = gemSlotDeltaEffectSummaries(slotId, upgradeable.rarity, gemRarity, nextGem, gemSourceName, [Stat.FarmingFortune], multiplier);
result.push({
title: getGemRarityName(nextGem) + ' Peridot Gemstone',
cost: {
items: {
[`${getGemRarityName(nextGem).toUpperCase()}_PERIDOT_GEM`]: 1,
},
},
onto: {
name: upgradeable.item.name,
skyblockId: upgradeable.item.skyblockId,
},
increase: nextFortune - currentFortune,
stats: {
[Stat.FarmingFortune]: nextFortune - currentFortune,
},
effects: effects.length > 0 ? effects : undefined,
action: UpgradeAction.Apply,
category: UpgradeCategory.Gem,
conflictKey: getItemScopedConflictKey(upgradeable, `gem:${slotId}:${nextGem}`),
meta: {
type: 'gem',
slot: slotId,
value: nextGem,
itemUuid: upgradeable.item.uuid ?? undefined,
},
});
}
}
return result;
}
function getGemStatMultiplier(upgradeable) {
return upgradeable.constructor.name === 'FarmingAccessory' ? 0.5 : 1;
}
function meetsGemSlotRequirements(upgradeable, slot) {
if (!slot)
return true;
const requirements = slot.requirements;
if (!requirements || requirements.length === 0)
return true;
for (const req of requirements) {
if (req.type !== 'ITEM_DATA')
continue;
const key = req.data_key;
const raw = upgradeable.item.attributes?.[key];
const current = toNumberOrDefault(raw, key === 'levelable_lvl' ? 1 : 0);
const target = toNumberOrDefault(req.value, 0);
switch (req.operator) {
case 'GREATER_THAN_OR_EQUALS':
if (!(current >= target))
return false;
break;
case 'GREATER_THAN':
if (!(current > target))
return false;
break;
case 'EQUALS':
if (!(current === target))
return false;
break;
case 'LESS_THAN_OR_EQUALS':
if (!(current <= target))
return false;
break;
case 'LESS_THAN':
if (!(current < target))
return false;
break;
default:
// Unknown operator: don't block suggestions.
break;
}
}
return true;
}
function toNumberOrDefault(value, defaultValue) {
const n = typeof value === 'number' ? value : typeof value === 'string' ? +value : NaN;
return Number.isFinite(n) ? n : defaultValue;
}
//# sourceMappingURL=upgrades.js.map