UNPKG

farming-weight

Version:

Tools for calculating farming weight and fortune in Hypixel Skyblock

608 lines 25.4 kB
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