UNPKG

farming-weight

Version:

Tools for calculating farming weight and fortune in Hypixel Skyblock

1,074 lines 46.7 kB
import { FARMING_ATTRIBUTE_SHARDS, getShardFortune, getShardLevel, getShardStat, getShardsForLevel, getShardsForNextLevel, } from '../../constants/attributes.js'; import { GARDEN_CHIP_MAX_LEVEL, GARDEN_CHIPS, getChipLevel, getChipRarity, } from '../../constants/chips.js'; import { CROP_INFO } from '../../constants/crops.js'; import { BESTIARY_PEST_BRACKETS, DEFAULT_GARDEN_BESTIARY_PEST_BRACKET, GARDEN_BESTIARY_BRACKETS, PEST_BESTIARY_IDS, } from '../../constants/pests.js'; import { Rarity } from '../../constants/reforges.js'; import { ANITA_FORTUNE_UPGRADE, COMMUNITY_CENTER_UPGRADE, DNA_MILESTONE_SOURCE, FARMING_LEVEL, FILLED_ROSEWATER_FLASK_SOURCE, PEST_BESTIARY_SOURCE, REFINED_TRUFFLE_SOURCE, UNLOCKED_PLOTS, WRIGGLING_LARVA_SOURCE, } from '../../constants/specific.js'; import { Stat } from '../../constants/stats.js'; import { UpgradeAction, UpgradeCategory } from '../../constants/upgrades.js'; import { buildEffectEnvironment } from '../../effects/environment.js'; import { effectsToSummaries } from '../../effects/summary.js'; import { FarmingAccessory } from '../../fortune/farmingaccessory.js'; import { FARMING_ACCESSORIES_INFO } from '../../items/accessories.js'; import { FARMING_ATTRIBUTE_SHARD_CLASSES } from '../../items/sources/attributes.js'; import { GARDEN_CHIP_CLASSES } from '../../items/sources/chips.js'; import { getNextPlotCost } from '../../util/garden.js'; import { fortuneFromPestBestiary, getGardenBestiaryProgress } from '../../util/pests.js'; import { getFortune } from '../getfortune.js'; import { getSourceProgress } from '../getsourceprogress.js'; export const GARDEN_CHIP_SOURCES = Object.entries(GARDEN_CHIPS).map(([chipId, chip]) => mapChipSource(chipId, chip)); function hasPlayerOptions(source) { return typeof source === 'object' && source !== null && 'options' in source; } function shardStat(shard, player, stat, level) { return getShardStat(shard, player, stat, level); } function attributeShardEffectSummaries(player, id, stats, amount) { if (!hasPlayerOptions(player)) return []; const source = FARMING_ATTRIBUTE_SHARD_CLASSES[id]; if (!source) return []; const effectPlayer = amount === undefined ? player : { ...player, options: { ...player.options, attributes: { ...player.options.attributes, [id]: amount, }, }, }; const env = buildEffectEnvironment(effectPlayer); return effectsToSummaries(source.getEffects(effectPlayer, env), stats); } function allAttributeShardEffectSummaries(player, stats) { const env = buildEffectEnvironment(player); return effectsToSummaries(Object.values(FARMING_ATTRIBUTE_SHARD_CLASSES).flatMap((source) => source.getEffects(player, env)), stats); } function gardenChipEffectSummaries(player, chipId, stats, level) { const source = GARDEN_CHIP_CLASSES[chipId]; const effectPlayer = level === undefined ? player : { ...player, options: { ...player.options, chips: { ...player.options.chips, [chipId]: level, }, }, }; const env = buildEffectEnvironment(effectPlayer); return effectsToSummaries(source.getEffects(effectPlayer, env), stats); } function getLevelDeltaStats(currentLevel, nextLevel, source) { const result = {}; // Always include FarmingFortune const ff = (source.fortunePerLevel ?? 0) * (nextLevel - currentLevel); if (ff !== 0) result[Stat.FarmingFortune] = ff; for (const [k, per] of Object.entries(source.statsPerLevel ?? {})) { const stat = k; const diff = (per ?? 0) * (nextLevel - currentLevel); if (diff !== 0) result[stat] = diff; } return result; } export const GENERAL_FORTUNE_SOURCES = [ { name: FARMING_LEVEL.name, wiki: () => FARMING_LEVEL.wiki, exists: () => true, max: () => FARMING_LEVEL.maxLevel * FARMING_LEVEL.fortunePerLevel, current: (player) => { return (player.options.farmingLevel ?? 0) * FARMING_LEVEL.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(FARMING_LEVEL.maxLevel, FARMING_LEVEL, stat), currentStat: (player, stat) => getFortune(player.options.farmingLevel, FARMING_LEVEL, stat), upgrades: (player) => { const current = player.options.farmingLevel ?? 0; if (current < 50 || current >= FARMING_LEVEL.maxLevel) return []; const nextCost = FARMING_LEVEL.upgradeCosts?.[current + 1]; if (!nextCost) return []; const stats = getLevelDeltaStats(current, current + 1, FARMING_LEVEL); return [ { title: FARMING_LEVEL.name + ' ' + (current + 1), increase: stats[Stat.FarmingFortune] ?? 0, stats, action: UpgradeAction.LevelUp, category: UpgradeCategory.Skill, wiki: FARMING_LEVEL.wiki, cost: nextCost, meta: { type: 'skill', key: 'farmingLevel', value: current + 1, }, }, ]; }, }, { name: 'Attribute Shards', wiki: () => 'https://w.elitesb.gg/Attributes', exists: () => true, max: () => { return ATTRIBUTE_FORTUNE_SOURCES.filter((shard) => !shard.active || shard.active(maxShardOptions).active).reduce((acc, shard) => { return acc + shard.max(maxShardOptions); }, 0); }, current: (player) => { return ATTRIBUTE_FORTUNE_SOURCES.reduce((acc, shard) => { return acc + shard.current(player); }, 0); }, maxStat: (_player, stat) => { if (stat === Stat.FarmingFortune) { return ATTRIBUTE_FORTUNE_SOURCES.filter((shard) => !shard.active || shard.active(maxShardOptions).active).reduce((acc, shard) => acc + shard.max(maxShardOptions), 0); } return Object.values(FARMING_ATTRIBUTE_SHARDS).reduce((acc, shard) => acc + shardStat(shard, maxShardOptions, stat, 10), 0); }, currentStat: (player, stat) => { if (stat === Stat.FarmingFortune) { return ATTRIBUTE_FORTUNE_SOURCES.reduce((acc, shard) => acc + shard.current(player), 0); } return Object.values(FARMING_ATTRIBUTE_SHARDS).reduce((acc, shard) => acc + shardStat(shard, player, stat), 0); }, effects: (player, stats) => allAttributeShardEffectSummaries(player, stats), upgrades: (player, stats) => { return ATTRIBUTE_SHARD_PROGRESS_SOURCES.filter((shard) => shard.active?.(player).active !== false) .flatMap((shard) => shard.upgrades?.(player, stats)) .filter(Boolean); }, progress: (player, stats) => { return getSourceProgress(player, ATTRIBUTE_SHARD_PROGRESS_SOURCES, false, stats); }, }, { name: 'Garden Chips', alwaysInclude: true, active: () => ({ active: true, reason: 'Garden Chips should be upgraded, but are hard to give fortune numbers for.', }), exists: () => true, max: () => { // Only chips with farming fortune increases const maxFortune = Object.values(GARDEN_CHIPS).reduce((acc, chip) => { const fortunePerLevel = chip.statsPerRarity?.[Rarity.Legendary]?.[Stat.FarmingFortune] ?? 0; return acc + fortunePerLevel * GARDEN_CHIP_MAX_LEVEL; }, 0); return maxFortune; }, current: (player) => { // Only return current fortune from chips with farming fortune increases const totalCurrent = Object.entries(GARDEN_CHIPS).reduce((acc, [chipId, chip]) => { const level = getChipLevel(player.options.chips?.[chipId]); const fortunePerLevel = chip.statsPerRarity?.[Rarity.Legendary]?.[Stat.FarmingFortune] ?? 0; return acc + fortunePerLevel * level; }, 0); return totalCurrent; }, maxStat: (player, stat) => { return Object.values(GARDEN_CHIPS).reduce((acc, chip) => { const per = chip.statsPerRarity?.[Rarity.Legendary]?.[stat] ?? 0; return acc + per * GARDEN_CHIP_MAX_LEVEL; }, 0); }, currentStat: (player, stat) => { return Object.entries(GARDEN_CHIPS).reduce((acc, [chipId, chip]) => { const level = getChipLevel(player.options.chips?.[chipId]); const rarity = getChipRarity(level); const per = chip.statsPerRarity?.[rarity]?.[stat] ?? 0; return acc + per * level; }, 0); }, progress: (player, stats) => { return getSourceProgress(player, GARDEN_CHIP_SOURCES, false, stats); }, upgrades: (player, stats) => { return GARDEN_CHIP_SOURCES.flatMap((source) => source.upgrades?.(player, stats)).filter(Boolean); }, }, { name: PEST_BESTIARY_SOURCE.name, wiki: () => PEST_BESTIARY_SOURCE.wiki, exists: () => true, max: () => PEST_BESTIARY_SOURCE.maxLevel * PEST_BESTIARY_SOURCE.fortunePerLevel, current: (player) => { return fortuneFromPestBestiary(player.options.bestiaryKills ?? {}); }, progress: (player, stats) => { const list = getGardenBestiaryProgress(player.options.bestiaryKills ?? {}); return Object.entries(list).map(([bestiaryId, pest]) => { const pestId = PEST_BESTIARY_IDS[bestiaryId] ?? null; const bracket = (pestId ? BESTIARY_PEST_BRACKETS[pestId] : GARDEN_BESTIARY_BRACKETS[bestiaryId]) ?? DEFAULT_GARDEN_BESTIARY_PEST_BRACKET; const totalBrackets = Object.keys(bracket).length; const maxFortune = totalBrackets * PEST_BESTIARY_SOURCE.fortunePerLevel; return { name: pest.name, current: pest.bracketsUnlocked * PEST_BESTIARY_SOURCE.fortunePerLevel, stats: { [Stat.FarmingFortune]: { current: pest.bracketsUnlocked * PEST_BESTIARY_SOURCE.fortunePerLevel, max: maxFortune, ratio: maxFortune === 0 ? 0 : (pest.bracketsUnlocked * PEST_BESTIARY_SOURCE.fortunePerLevel) / maxFortune, }, }, max: maxFortune, ratio: totalBrackets === 0 ? 0 : pest.bracketsUnlocked / totalBrackets, }; }); }, }, { name: ANITA_FORTUNE_UPGRADE.name, wiki: () => ANITA_FORTUNE_UPGRADE.wiki, exists: () => true, max: () => ANITA_FORTUNE_UPGRADE.maxLevel * ANITA_FORTUNE_UPGRADE.fortunePerLevel, current: (player) => { return (player.options.anitaBonus ?? 0) * ANITA_FORTUNE_UPGRADE.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(ANITA_FORTUNE_UPGRADE.maxLevel, ANITA_FORTUNE_UPGRADE, stat), currentStat: (player, stat) => getFortune(player.options.anitaBonus, ANITA_FORTUNE_UPGRADE, stat), upgrades: (player) => { const current = player.options.anitaBonus ?? 0; if (current >= ANITA_FORTUNE_UPGRADE.maxLevel) return []; const nextCost = ANITA_FORTUNE_UPGRADE.upgradeCosts?.[current + 1]; if (!nextCost) return []; const stats = getLevelDeltaStats(current, current + 1, ANITA_FORTUNE_UPGRADE); return [ { title: ANITA_FORTUNE_UPGRADE.name, increase: stats[Stat.FarmingFortune] ?? 0, stats, action: UpgradeAction.Upgrade, category: UpgradeCategory.Anita, wiki: ANITA_FORTUNE_UPGRADE.wiki, cost: nextCost, meta: { type: 'skill', key: 'anitaBonus', value: current + 1, }, }, ]; }, }, { name: UNLOCKED_PLOTS.name, wiki: () => UNLOCKED_PLOTS.wiki, exists: () => true, max: () => UNLOCKED_PLOTS.maxLevel * UNLOCKED_PLOTS.fortunePerLevel, current: (player) => { return (player.options.plots?.length ?? player.options.plotsUnlocked ?? 0) * UNLOCKED_PLOTS.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(UNLOCKED_PLOTS.maxLevel, UNLOCKED_PLOTS, stat), currentStat: (player, stat) => getFortune(player.options.plots?.length ?? player.options.plotsUnlocked, UNLOCKED_PLOTS, stat), upgrades: (player) => { const plotUpgrade = getNextPlotCost(player.options.plots ?? []); if (!plotUpgrade) return []; const current = player.options.plots?.length ?? player.options.plotsUnlocked ?? 0; const stats = getLevelDeltaStats(current, current + 1, UNLOCKED_PLOTS); return [ { title: 'Plot ' + plotUpgrade.plot?.name, increase: stats[Stat.FarmingFortune] ?? 0, stats, action: UpgradeAction.Purchase, category: UpgradeCategory.Plot, cost: plotUpgrade.cost, meta: { type: 'plot', id: plotUpgrade.plotId, value: current + 1, }, }, ]; }, }, { name: COMMUNITY_CENTER_UPGRADE.name, api: false, wiki: () => COMMUNITY_CENTER_UPGRADE.wiki, exists: () => true, max: () => COMMUNITY_CENTER_UPGRADE.maxLevel * COMMUNITY_CENTER_UPGRADE.fortunePerLevel, current: (player) => { return (player.options.communityCenter ?? 0) * COMMUNITY_CENTER_UPGRADE.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(COMMUNITY_CENTER_UPGRADE.maxLevel, COMMUNITY_CENTER_UPGRADE, stat), currentStat: (player, stat) => getFortune(player.options.communityCenter, COMMUNITY_CENTER_UPGRADE, stat), upgrades: (player) => { const current = player.options.communityCenter ?? 0; if (current >= COMMUNITY_CENTER_UPGRADE.maxLevel) return []; const stats = getLevelDeltaStats(current, current + 1, COMMUNITY_CENTER_UPGRADE); return [ { title: COMMUNITY_CENTER_UPGRADE.name, increase: stats[Stat.FarmingFortune] ?? 0, stats, action: UpgradeAction.Upgrade, repeatable: COMMUNITY_CENTER_UPGRADE.maxLevel - current, api: false, category: UpgradeCategory.CommunityCenter, wiki: COMMUNITY_CENTER_UPGRADE.wiki, meta: { type: 'skill', key: 'communityCenter', value: current + 1, }, }, ]; }, }, { name: 'Helianthus Relic', exists: (player) => { if (!player.options.selectedCrop) return true; const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FERMENTO_ARTIFACT?.family); // If player has Helianthus Relic, always show this if (highest?.info.skyblockId === 'HELIANTHUS_RELIC') return true; const cropFortuneType = CROP_INFO[player.options.selectedCrop]?.fortuneType; if (!highest || (cropFortuneType && highest.info.baseStats?.[cropFortuneType])) { return false; } return true; }, wiki: (player) => { const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FERMENTO_ARTIFACT?.family); return highest?.info.wiki ?? FARMING_ACCESSORIES_INFO.CROPIE_TALISMAN?.wiki; }, max: () => { const accessory = FarmingAccessory.fakeItem(FARMING_ACCESSORIES_INFO.CROPIE_TALISMAN); return accessory?.getLastItemUpgrade()?.info.baseStats?.[Stat.FarmingFortune] ?? 0; }, current: (player) => { const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FERMENTO_ARTIFACT?.family); if (!highest) return 0; if (highest.info.crops) { return 0; } return highest.info.baseStats?.[Stat.FarmingFortune] ?? 0; }, info: (player) => { const highest = player.activeAccessories.find((a) => a.info === FARMING_ACCESSORIES_INFO.HELIANTHUS_RELIC); const first = !highest ? FARMING_ACCESSORIES_INFO.CROPIE_TALISMAN : undefined; return { item: highest?.item, info: highest?.info, nextInfo: first ?? highest?.getNextItemUpgrade()?.info, maxInfo: highest?.getLastItemUpgrade()?.info ?? FARMING_ACCESSORIES_INFO.HELIANTHUS_RELIC, }; }, upgrades: (player) => { const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.HELIANTHUS_RELIC?.family); if (!highest) { const cropie = FARMING_ACCESSORIES_INFO.CROPIE_TALISMAN; if (!cropie) return []; return [ { title: cropie.name, increase: cropie.baseStats?.[Stat.FarmingFortune] ?? 0, stats: { [Stat.FarmingFortune]: cropie.baseStats?.[Stat.FarmingFortune] ?? 0, }, action: UpgradeAction.Purchase, item: 'CROPIE_TALISMAN', category: UpgradeCategory.Item, wiki: cropie.wiki, cost: { items: { CROPIE_TALISMAN: 1, }, }, meta: { type: 'buy_item', id: 'CROPIE_TALISMAN', }, conflictKey: 'accessory:CROPIE_TALISMAN', }, ]; } return highest.getUpgrades(); }, }, { name: 'Freshly Baked Heirloom', exists: () => true, wiki: (player) => { const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_HEIRLOOM?.family); return highest?.info.wiki ?? FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_TALISMAN?.wiki; }, // Overbloom-only source; doesn't contribute to Farming Fortune. max: () => 0, current: () => 0, maxStat: (player, stat) => { if (stat !== Stat.Overbloom) return 0; const accessory = FarmingAccessory.fakeItem(FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_TALISMAN); const last = accessory?.getLastItemUpgrade()?.info; return last ? (FarmingAccessory.fakeItem(last, player.options)?.getStat(Stat.Overbloom) ?? 0) : 0; }, currentStat: (player, stat) => { if (stat !== Stat.Overbloom) return 0; const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_HEIRLOOM?.family); return highest?.getStat(Stat.Overbloom) ?? 0; }, info: (player) => { const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_HEIRLOOM?.family); const first = !highest ? FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_TALISMAN : undefined; return { item: highest?.item, info: highest?.info, nextInfo: first ?? highest?.getNextItemUpgrade()?.info, maxInfo: highest?.getLastItemUpgrade()?.info ?? FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_HEIRLOOM, }; }, upgrades: (player) => { const highest = player.activeAccessories.find((a) => a.info.family === FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_HEIRLOOM?.family); if (!highest) { const talisman = FARMING_ACCESSORIES_INFO.FRESHLY_BAKED_TALISMAN; if (!talisman) return []; return [ { title: talisman.name, increase: 0, stats: { [Stat.Overbloom]: FarmingAccessory.fakeItem(talisman, player.options)?.getStat(Stat.Overbloom) ?? 0, }, action: UpgradeAction.Purchase, item: 'FRESHLY_BAKED_TALISMAN', category: UpgradeCategory.Item, wiki: talisman.wiki, cost: { kernels: 25, }, meta: { type: 'buy_item', id: 'FRESHLY_BAKED_TALISMAN', }, conflictKey: 'accessory:FRESHLY_BAKED_TALISMAN', }, ]; } return highest.getUpgrades({ stat: Stat.Overbloom }); }, }, { name: DNA_MILESTONE_SOURCE.name, wiki: () => DNA_MILESTONE_SOURCE.wiki, exists: () => true, max: () => DNA_MILESTONE_SOURCE.maxLevel * DNA_MILESTONE_SOURCE.fortunePerLevel, current: (player) => { return (player.options.dnaMilestone ?? 0) * DNA_MILESTONE_SOURCE.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(DNA_MILESTONE_SOURCE.maxLevel, DNA_MILESTONE_SOURCE, stat), currentStat: (player, stat) => getFortune(player.options.dnaMilestone ?? 0, DNA_MILESTONE_SOURCE, stat), upgrades: (player) => { const level = player.options.dnaMilestone ?? 0; if (level >= DNA_MILESTONE_SOURCE.maxLevel) return []; return [ { title: 'DNA Analysis Milestone ' + (level + 1), increase: DNA_MILESTONE_SOURCE.fortunePerLevel, stats: { [Stat.FarmingFortune]: DNA_MILESTONE_SOURCE.fortunePerLevel, }, action: UpgradeAction.LevelUp, wiki: DNA_MILESTONE_SOURCE.wiki, category: UpgradeCategory.Milestone, meta: { type: 'setting', key: 'dnaMilestone', value: level + 1, }, }, ]; }, }, { name: 'Relic of Power', exists: () => true, wiki: () => FARMING_ACCESSORIES_INFO.POWER_RELIC?.wiki, max: () => { const accessory = FarmingAccessory.fakeItem(FARMING_ACCESSORIES_INFO.POWER_RELIC); return accessory?.getProgress()?.reduce((acc, p) => acc + p.max, 0) ?? 0; }, current: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'POWER_RELIC'); return accessory?.fortune ?? 0; }, info: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'POWER_RELIC'); const fake = !accessory ? FarmingAccessory.fakeItem(FARMING_ACCESSORIES_INFO.POWER_RELIC) : undefined; return { item: accessory?.item, info: accessory?.info, nextInfo: fake ? fake.info : accessory?.getNextItemUpgrade()?.info, maxInfo: (fake ? fake : accessory)?.getLastItemUpgrade()?.info, }; }, upgrades: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'POWER_RELIC'); if (!accessory) return [ { title: 'Relic of Power', increase: 0, stats: { [Stat.FarmingFortune]: 0, }, action: UpgradeAction.Purchase, purchase: 'POWER_RELIC', category: UpgradeCategory.Item, wiki: FARMING_ACCESSORIES_INFO.POWER_RELIC?.wiki, cost: FARMING_ACCESSORIES_INFO.POWER_RELIC?.cost, meta: { type: 'buy_item', id: 'POWER_RELIC', }, conflictKey: 'accessory:POWER_RELIC', }, ]; return accessory.getUpgrades(); }, }, { name: 'Magic 8 Ball', exists: () => true, wiki: () => 'https://w.elitesb.gg/Magic_8_Ball', max: () => 25, active: () => { return { active: true, reason: 'Magic 8 Ball only has a 20% chance to be active each season.', }; }, current: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'MAGIC_8_BALL'); return accessory ? 25 : 0; }, upgrades: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'MAGIC_8_BALL'); if (!accessory) return [ { title: 'Magic 8 Ball', increase: 25 * 0.2, stats: { [Stat.FarmingFortune]: 25 * 0.2, }, action: UpgradeAction.Purchase, purchase: 'MAGIC_8_BALL', category: UpgradeCategory.Item, wiki: 'https://w.elitesb.gg/Magic_8_Ball', cost: { items: { MAGIC_8_BALL: 1, }, }, meta: { type: 'buy_item', id: 'MAGIC_8_BALL', }, conflictKey: 'accessory:MAGIC_8_BALL', }, ]; return []; }, }, { name: 'Atmospheric Filter', exists: () => true, wiki: () => 'https://w.elitesb.gg/Atmospheric_Filter', max: () => 25, active: () => { return { active: true, reason: 'Atmospheric Filter only gives fortune during the Spring season.', }; }, current: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'ATMOSPHERIC_FILTER'); return accessory ? 25 : 0; }, upgrades: (player) => { const accessory = player.accessories.find((a) => a.info.skyblockId === 'ATMOSPHERIC_FILTER'); if (accessory) return []; return [ { title: 'Atmospheric Filter', increase: 25 * 0.25, stats: { [Stat.FarmingFortune]: 25 * 0.25, }, action: UpgradeAction.Purchase, purchase: 'ATMOSPHERIC_FILTER', category: UpgradeCategory.Item, wiki: 'https://w.elitesb.gg/Atmospheric_Filter', cost: { items: { ATMOSPHERIC_FILTER: 1, }, }, meta: { type: 'buy_item', id: 'ATMOSPHERIC_FILTER', }, conflictKey: 'accessory:ATMOSPHERIC_FILTER', }, ]; }, }, { name: REFINED_TRUFFLE_SOURCE.name, wiki: () => REFINED_TRUFFLE_SOURCE.wiki, exists: () => true, max: () => REFINED_TRUFFLE_SOURCE.maxLevel * REFINED_TRUFFLE_SOURCE.fortunePerLevel, current: (player) => { return (player.options.refinedTruffles ?? 0) * REFINED_TRUFFLE_SOURCE.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(REFINED_TRUFFLE_SOURCE.maxLevel, REFINED_TRUFFLE_SOURCE, stat), currentStat: (player, stat) => getFortune(player.options.refinedTruffles ?? 0, REFINED_TRUFFLE_SOURCE, stat), upgrades: (player) => { const consumed = player.options.refinedTruffles ?? 0; if (consumed >= 5) return []; return [ { title: 'Refined Dark Cacao Truffle', increase: REFINED_TRUFFLE_SOURCE.fortunePerLevel, stats: { [Stat.FarmingFortune]: REFINED_TRUFFLE_SOURCE.fortunePerLevel, }, action: UpgradeAction.Consume, repeatable: 5 - consumed, wiki: REFINED_TRUFFLE_SOURCE.wiki, category: UpgradeCategory.Item, cost: { items: { REFINED_DARK_CACAO_TRUFFLE: 1, }, }, meta: { type: 'setting', key: 'refinedTruffles', value: consumed + 1, }, }, ]; }, }, { name: FILLED_ROSEWATER_FLASK_SOURCE.name, api: false, wiki: () => FILLED_ROSEWATER_FLASK_SOURCE.wiki, exists: () => true, max: () => FILLED_ROSEWATER_FLASK_SOURCE.maxLevel * FILLED_ROSEWATER_FLASK_SOURCE.fortunePerLevel, current: (player) => { return (player.options.filledRosewaterFlask ?? 0) * FILLED_ROSEWATER_FLASK_SOURCE.fortunePerLevel; }, maxStat: (_player, stat) => getFortune(FILLED_ROSEWATER_FLASK_SOURCE.maxLevel, FILLED_ROSEWATER_FLASK_SOURCE, stat), currentStat: (player, stat) => getFortune(player.options.filledRosewaterFlask ?? 0, FILLED_ROSEWATER_FLASK_SOURCE, stat), upgrades: (player) => { const consumed = player.options.filledRosewaterFlask ?? 0; if (consumed >= 5) return []; return [ { title: 'Filled Rosewater Flask', increase: FILLED_ROSEWATER_FLASK_SOURCE.fortunePerLevel, stats: { [Stat.FarmingFortune]: FILLED_ROSEWATER_FLASK_SOURCE.fortunePerLevel, }, api: false, action: UpgradeAction.Consume, repeatable: 5 - consumed, wiki: FILLED_ROSEWATER_FLASK_SOURCE.wiki, category: UpgradeCategory.Item, cost: { items: { FILLED_ROSEWATER_FLASK: 1, }, }, meta: { type: 'setting', key: 'filledRosewaterFlasks', value: consumed + 1, }, }, ]; }, }, { name: WRIGGLING_LARVA_SOURCE.name, wiki: () => WRIGGLING_LARVA_SOURCE.wiki, exists: () => true, max: () => 0, current: () => 0, maxStat: (_player, stat) => getFortune(WRIGGLING_LARVA_SOURCE.maxLevel, WRIGGLING_LARVA_SOURCE, stat), currentStat: (player, stat) => getFortune(player.options.wrigglingLarva ?? 0, WRIGGLING_LARVA_SOURCE, stat), upgrades: (player, stats) => { if (!stats?.includes(Stat.BonusPestChance)) return []; const consumed = player.options.wrigglingLarva ?? 0; if (consumed >= 5) return []; return [ { title: 'Wriggling Larva', increase: 0, stats: { [Stat.BonusPestChance]: WRIGGLING_LARVA_SOURCE.statsPerLevel?.[Stat.BonusPestChance] ?? 0, }, action: UpgradeAction.Consume, repeatable: 5 - consumed, wiki: WRIGGLING_LARVA_SOURCE.wiki, category: UpgradeCategory.Item, cost: { items: { WRIGGLING_LARVA: 1, }, }, meta: { type: 'setting', key: 'wrigglingLarva', value: consumed + 1, }, }, ]; }, }, ...createCarnivalHarvestFeastSources(), ]; function getCarnivalPerkLevel(player, key) { const raw = player.options.harvestFeast?.perks?.[key] ?? player.options.perks?.[key]; if (raw === null || raw === undefined) return 0; const level = typeof raw === 'number' ? raw : parseInt(raw, 10); return Number.isFinite(level) ? level : 0; } function getCarnivalPerkFortune(player, perk) { return getCarnivalPerkLevel(player, perk.key) * perk.perLevel; } function getCarnivalPerkEffect(player, perk) { const level = getCarnivalPerkLevel(player, perk.key); if (level <= 0) return undefined; return perk.effect?.(level); } function isHarvestFeastActive(player) { return player.options.harvestFeast?.active === true; } function createCarnivalHarvestFeastSources() { const perks = [ { key: 'natural_talent', name: 'Natural Talent', wiki: 'https://w.elitesb.gg/Doug', maxLevel: 5, perLevel: 1, effect: (level) => ({ source: 'Natural Talent', op: 'add-rare-pct', value: level, scope: { tags: ['seasoning'], requiresHarvestFeast: true }, meta: { description: '+1% Seasoning chance per level during Harvest Feast', valueDisplay: 'percent', }, }), }, { key: 'fortunate_feasting', name: 'Fortunate Feasting', wiki: 'https://w.elitesb.gg/Doug', stat: Stat.FarmingFortune, maxLevel: 5, perLevel: 5, }, ]; return perks.map((perk) => ({ name: perk.name, wiki: () => perk.wiki, exists: () => true, active: (player) => { if (isHarvestFeastActive(player)) return { active: true }; return { active: false, reason: 'Only active during a Harvest Feast.', fortune: perk.stat === Stat.FarmingFortune ? getCarnivalPerkFortune(player, perk) : 0, }; }, max: () => (perk.stat === Stat.FarmingFortune ? perk.maxLevel * perk.perLevel : 0), current: (player) => { if (perk.stat !== Stat.FarmingFortune) return 0; if (!isHarvestFeastActive(player)) return 0; return getCarnivalPerkFortune(player, perk); }, maxStat: (_player, stat) => (stat === perk.stat ? perk.maxLevel * perk.perLevel : 0), currentStat: (player, stat) => { if (stat !== perk.stat || !isHarvestFeastActive(player)) return 0; return getCarnivalPerkFortune(player, perk); }, activeStat: (player, stat) => { if (stat !== perk.stat) return { active: isHarvestFeastActive(player) }; return { active: isHarvestFeastActive(player), value: getCarnivalPerkFortune(player, perk), }; }, calculationEffects: (player) => { const effect = getCarnivalPerkEffect(player, perk); return effect ? [effect] : []; }, effects: (player) => { const effect = getCarnivalPerkEffect(player, perk); return effect ? [ { source: effect.source, op: effect.op, value: typeof effect.value === 'number' ? effect.value : undefined, scope: effect.scope, description: effect.meta?.description, valueDisplay: effect.meta?.valueDisplay, valueStat: effect.meta?.valueStat, }, ] : []; }, progress: (player) => { const level = getCarnivalPerkLevel(player, perk.key); if (level <= 0) return undefined; return [ { name: 'Level', current: level, max: perk.maxLevel, maxLevel: perk.maxLevel, fortunePerLevel: perk.perLevel, ratio: Math.min(level / perk.maxLevel, 1), }, ]; }, })); } export const ATTRIBUTE_FORTUNE_SOURCES = Object.entries(FARMING_ATTRIBUTE_SHARDS) .filter((a) => a[1].effect === 'fortune') .map(([id, shard]) => mapShardSource(id, shard)); // Includes all shards (fortune + non-fortune effects) so per-stat progress and upgrades // can surface shards like Cropeetle that contribute non-FarmingFortune stats. export const ATTRIBUTE_SHARD_PROGRESS_SOURCES = Object.entries(FARMING_ATTRIBUTE_SHARDS).map(([id, shard]) => mapShardSource(id, shard)); const maxShardOptions = { attributes: Object.fromEntries(Object.keys(FARMING_ATTRIBUTE_SHARDS).map((id) => [ id, 1000, // Max level for all shards ])), blocksBroken: 0, crop: 'CACTUS', bountiful: false, mooshroom: false, infestedPlotProbability: 1, }; function mapShardSource(id, shard) { const result = { name: shard.name, wiki: () => shard.wiki, exists: () => true, active: shard.active, max: () => { return getShardFortune(shard, { ...maxShardOptions, attributes: { [id]: 1000 }, }, 10); }, current: (player) => { return getShardFortune(shard, player); }, maxStat: (_player, stat) => { if (stat === Stat.FarmingFortune) { return getShardFortune(shard, { ...maxShardOptions, attributes: { [id]: 1000 }, }, 10); } return shardStat(shard, { ...maxShardOptions, attributes: { [id]: 1000 }, }, stat, 10); }, currentStat: (player, stat) => { if (stat === Stat.FarmingFortune) return getShardFortune(shard, player); return shardStat(shard, player, stat); }, effects: (player, stats) => attributeShardEffectSummaries(player, id, stats), upgrades: (player, stats) => { const amount = player.attributes?.[id] ?? 0; const nextCost = getShardsForNextLevel(shard.rarity, amount); if (!nextCost) return []; const currentLevel = getShardLevel(shard.rarity, amount); const level = currentLevel + 1; const deltaStats = {}; for (const s of Object.values(Stat)) { const before = shardStat(shard, player, s, currentLevel); const after = shardStat(shard, player, s, level); const diff = after - before; if (diff !== 0) deltaStats[s] = diff; } const effects = attributeShardEffectSummaries(player, id, stats, getShardsForLevel(shard.rarity, level)); // If a specific stat is requested, only surface this shard when it affects it. if (stats && stats.length > 0) { const affects = stats.some((s) => (deltaStats[s] ?? 0) !== 0); if (!affects && effects.length === 0) return []; } const increase = deltaStats[Stat.FarmingFortune] ?? 0; return [ { title: shard.name.replace('Shard', level.toString()), increase, stats: Object.keys(deltaStats).length > 0 ? deltaStats : undefined, effects: effects.length > 0 ? effects : undefined, action: UpgradeAction.LevelUp, category: UpgradeCategory.Attribute, // wiki: shard.wiki, // Wiki page doesn't exist yet cost: { items: { [shard.skyblockId]: nextCost, }, }, meta: { type: 'attribute', key: id, value: getShardsForLevel(shard.rarity, level), }, }, ]; }, }; return result; } function mapChipSource(chipId, chip) { return { name: chip.name, alwaysInclude: true, wiki: () => chip.wiki, exists: () => true, // Progress-only display (avoid default Farming Fortune icon): show level in nested progress. max: () => 0, current: () => 0, maxStat: (player, stat) => { const per = chip.statsPerRarity?.[Rarity.Legendary]?.[stat] ?? 0; return per * GARDEN_CHIP_MAX_LEVEL; }, currentStat: (player, stat) => { const level = getChipLevel(player.options.chips?.[chipId]); const rarity = getChipRarity(level); const per = chip.statsPerRarity?.[rarity]?.[stat] ?? 0; return per * level; }, effects: (player, stats) => gardenChipEffectSummaries(player, chipId, stats), active: (player) => { if (chip.skyblockId !== 'OVERDRIVE_GARDEN_CHIP') return { active: true }; if (!player.options.jacobContest?.enabled) { return { active: false, reason: "Overdrive only applies during Jacob's Contest." }; } if (!player.options.jacobContest.crop) { return { active: false, reason: "Select an active Jacob's Contest crop to apply Overdrive." }; } return { active: true, reason: `Applies to ${player.options.jacobContest.crop} during contest.` }; }, progress: (player) => { const level = getChipLevel(player.options.chips?.[chipId]); return [ { name: 'Level', current: level, max: GARDEN_CHIP_MAX_LEVEL, ratio: Math.min(isNaN(level / GARDEN_CHIP_MAX_LEVEL) ? 0 : level / GARDEN_CHIP_MAX_LEVEL, 1), }, ]; }, upgrades: (player, stats) => { const currentLevel = getChipLevel(player.options.chips?.[chipId]); if (currentLevel >= GARDEN_CHIP_MAX_LEVEL) return []; const nextLevel = currentLevel + 1; const effects = gardenChipEffectSummaries(player, chipId, stats, nextLevel); // If a specific stat is requested, only offer upgrades that affect it. if (stats && stats.length > 0 && chip.statsPerRarity) { const affectsRequested = stats.some((s) => (chip.statsPerRarity?.[Rarity.Legendary]?.[s] ?? 0) !== 0); if (!affectsRequested && effects.length === 0) return []; } const deltaStats = {}; const currentRarity = getChipRarity(currentLevel); const nextRarity = getChipRarity(nextLevel); for (const [k, v] of Object.entries(chip.statsPerRarity?.[nextRarity] ?? {})) { const stat = k; const before = (chip.statsPerRarity?.[currentRarity]?.[stat] ?? 0) * currentLevel; const after = (v ?? 0) * nextLevel; const diff = after - before; if (diff !== 0) deltaStats[stat] = diff; } return [ { title: `${chip.name} ${nextLevel}`, increase: deltaStats[Stat.FarmingFortune] ?? 0, stats: Object.keys(deltaStats).length > 0 ? deltaStats : undefined, effects: effects.length > 0 ? effects : undefined, action: UpgradeAction.LevelUp, category: UpgradeCategory.Misc, cost: { items: { [chip.skyblockId]: 1, }, }, meta: { type: 'chip', id: chip.skyblockId, value: nextLevel, }, }, ]; }, }; } //# sourceMappingURL=generalsources.js.map