UNPKG

farming-weight

Version:

Tools for calculating farming weight and fortune in Hypixel Skyblock

358 lines 14.1 kB
import { CROP_INFO } from '../constants/crops.js'; import { FARMING_ENCHANTS } from '../constants/enchants.js'; import { REFORGES, ReforgeTarget } from '../constants/reforges.js'; import { Stat } from '../constants/stats.js'; import { TOOL_EXP_LEVELS } from '../constants/toollevels.js'; import { getQueryStats, } from '../constants/upgrades.js'; import { statsToEffects } from '../items/sources/effects-util.js'; import { enchantEffects } from '../items/sources/enchants.js'; import { gemEffects } from '../items/sources/gems.js'; import { reforgeEffects } from '../items/sources/reforges.js'; import { FARMING_TOOLS, FarmingToolType } from '../items/tools.js'; import { getSourceProgress } from '../upgrades/getsourceprogress.js'; import { TOOL_FORTUNE_SOURCES } from '../upgrades/sources/toolsources.js'; import { getSelfFortuneUpgrade, getUpgradeableRarityUpgrade } from '../upgrades/upgrades.js'; import { filterAndSortUpgrades } from '../upgrades/upgradeutils.js'; import { getFortuneFromEnchant, getStatFromEnchant } from '../util/enchants.js'; import { getLevel } from '../util/garden.js'; import { getGemStat, getPeridotFortune } from '../util/gems.js'; import { getRarityFromItem } from '../util/itemstats.js'; import { UpgradeableBase } from './upgradeablebase.js'; export class FarmingTool extends UpgradeableBase { get type() { return ReforgeTarget.FarmingTool; } // Backwards compatibility get tool() { return this.info; } get name() { return this.colorPrefix + (this.reforge?.name ?? '') + ' ' + this.itemname; } get bountiful() { return this.reforge?.name === REFORGES.bountiful?.name; } /** * Tool XP (`levelable_exp`) is XP within the current level and resets on level-up. * When a tool is capped it can show >= 100% progress but not advance. */ getCurrentLevelProgress() { const currentLevel = Math.max(1, Math.floor(this.level || 1)); const rawXp = this.xp || 0; if (currentLevel >= 50) { return { level: 50, next: undefined, total: rawXp, progress: 0, goal: undefined, ratio: 1, maxed: true, }; } const goal = TOOL_EXP_LEVELS[currentLevel - 1] ?? 0; if (!goal) { return { level: currentLevel, next: undefined, total: rawXp, progress: Math.max(0, rawXp), goal: undefined, ratio: 0, maxed: false, }; } const clamped = Math.max(0, Math.min(rawXp, goal)); const stats = getLevel(clamped, [goal], 1, false); const cappedAtThisLevel = rawXp >= goal; return { level: currentLevel, next: currentLevel + 1, total: rawXp, progress: stats.progress, goal, ratio: cappedAtThisLevel ? 1 : stats.ratio, maxed: cappedAtThisLevel, }; } constructor(item, options) { super({ item, options, items: FARMING_TOOLS }); this.rebuildTool(item, options); } getProgress(stats, zeroed = false) { return getSourceProgress(this, TOOL_FORTUNE_SOURCES, zeroed, stats); } getUpgrades(options) { const { deadEnd, upgrade: self } = getSelfFortuneUpgrade(this) ?? {}; if (deadEnd && self) return filterAndSortUpgrades([self], options); const stats = getQueryStats(options, [Stat.FarmingFortune, Stat.Overbloom]); const upgrades = getSourceProgress(this, TOOL_FORTUNE_SOURCES, false, stats).flatMap((source) => source.upgrades ?? []); if (self) { upgrades.push(self); } const rarityUpgrade = getUpgradeableRarityUpgrade(this); if (rarityUpgrade) { upgrades.push(rarityUpgrade); } return filterAndSortUpgrades(upgrades, options); } setOptions(options) { this.options = options; this.fortune = this.getFortune(); } rebuildTool(item, options) { this.options = options; this.item = item; const tool = FARMING_TOOLS[item.skyblockId]; if (!tool) { throw new Error(`Unknown farming tool: ${item.name} (${item.skyblockId})`); } this.info = tool; this.crops = tool.crops ?? (tool.crop ? [tool.crop] : []); // Set the base class crop property for backwards compatibility with Upgradeable interface this.crop = this.crops[0]; this.rarity = getRarityFromItem(item, this.rarity); this.counter = this.getCounter(); this.cultivating = this.getCultivating() ?? 0; this.logCounter = 0; this.collAnalysis = 0; this.setReforge(item.attributes?.modifier ?? ''); this.farmingForDummies = +(this.item.attributes?.farming_for_dummies_count ?? 0); this.recombobulated = this.item.attributes?.rarity_upgrades === '1'; this.overclocks = this.getOverclocks(); this.level = this.getToolLevel(); this.xp = this.getXp(); this.fortune = this.getFortune(); if (this.reforge?.name && item.name) { const [prefix, name] = item.name.split(this.reforge.name); this.colorPrefix = prefix ?? ''; this.itemname = name?.trim() ?? ''; } else { this.colorPrefix = ''; this.itemname = item.name ?? ''; } } setReforge(reforgeId) { this.reforge = REFORGES[reforgeId] ?? undefined; this.reforgeStats = this.reforge?.tiers?.[this.rarity]; } changeReforgeTo(reforgeId) { this.setReforge(reforgeId); this.fortune = this.getFortune(); } changeFarmedCropsTo(crops) { if (this.tool.type !== FarmingToolType.MathematicalHoe) return; const digits = Math.floor(crops).toString().length - 4; if (this.logCounter > 0) { this.logCounter = digits * 16; } if (this.collAnalysis > 0) { this.collAnalysis = digits * 8; } this.fortune = this.getFortune(); } getStat(stat, selectedCrop) { let sum = 0; const crops = selectedCrop && this.crops.includes(selectedCrop) ? [selectedCrop] : this.crops; // Tool level gives 4 crop-specific fortune per level for (const crop of crops) { if (stat === CROP_INFO[crop]?.fortuneType) { sum += this.level * 4; break; } } if (stat === Stat.FarmingFortune) { sum += this.farmingForDummies; } // Tools now have a flat +1 Farming Wisdom baseline. if (stat === Stat.FarmingWisdom) { sum += 1; } // Gems sum += getGemStat(this.item, stat, this.rarity); // Reforge stats sum += this.reforgeStats?.stats?.[stat] ?? 0; // Enchantments const enchantments = Object.entries(this.item.enchantments ?? {}); for (const [enchant, level] of enchantments) { if (!level) continue; const enchantment = FARMING_ENCHANTS[enchant]; if (!enchantment || !level) continue; if (enchantment.cropSpecific && !this.crops.includes(enchantment.cropSpecific)) continue; for (const crop of crops) { const val = getStatFromEnchant(level, enchantment, stat, this.options, crop); sum += val; } } return sum; } /** * Returns the declarative `Effect[]` representation of every contribution * this tool makes. Tool-level fortune (per-crop), Farming for Dummies, * baseline Farming Wisdom, gems, reforge, enchants. The tool-level fortune * uses the tool's own `crops` list to emit one `add-stat` per crop fortune * type. The resolver applies these unconditionally; the calculator picks * the appropriate per-crop fortune based on the active crop. */ getEffects(env) { const sourceName = this.item.name ?? this.info.name; const effects = []; const levelStats = {}; for (const crop of this.crops) { const cropStat = CROP_INFO[crop]?.fortuneType; if (!cropStat) continue; levelStats[cropStat] = (levelStats[cropStat] ?? 0) + this.level * 4; } effects.push(...statsToEffects(levelStats, `${sourceName} (Tool Level)`)); if (this.farmingForDummies > 0) { effects.push({ source: `${sourceName} (Farming for Dummies)`, op: 'add-stat', stat: Stat.FarmingFortune, value: this.farmingForDummies, }); } effects.push({ source: sourceName, op: 'add-stat', stat: Stat.FarmingWisdom, value: 1, }); effects.push(...gemEffects(this.item, this.rarity, `${sourceName} (Gems)`)); if (this.reforge && this.item.attributes?.modifier) { effects.push(...reforgeEffects(this.item.attributes.modifier, this.rarity, `${sourceName} (${this.reforge.name})`)); } for (const [enchantId, level] of Object.entries(this.item.enchantments ?? {})) { if (!level) continue; const enchantment = FARMING_ENCHANTS[enchantId]; if (enchantment?.cropSpecific && !this.crops.includes(enchantment.cropSpecific)) continue; effects.push(...enchantEffects(enchantId, level, env, this.options ?? {})); } return effects; } getFortune() { this.fortuneBreakdown = {}; let sum = 0; // Tool level (4 Farming Fortune per tool level) const levelFortune = this.level * 4; this.fortuneBreakdown['Tool Level'] = levelFortune; sum += levelFortune; // Reforge stats const reforge = this.reforgeStats?.stats?.[Stat.FarmingFortune] ?? 0; if (reforge > 0) { this.fortuneBreakdown[this.reforge?.name ?? 'Reforge'] = reforge; sum += reforge; } // Farming for Dummies if (this.farmingForDummies > 0) { this.fortuneBreakdown['Farming for Dummies'] = this.farmingForDummies; sum += this.farmingForDummies; } // Gems const peridot = getPeridotFortune(this.rarity, this.item); if (peridot > 0) { this.fortuneBreakdown['Peridot Gems'] = peridot; sum += peridot; } // Enchantments const enchantments = Object.entries(this.item.enchantments ?? {}); for (const [enchant, level] of enchantments) { if (!level) continue; const enchantment = FARMING_ENCHANTS[enchant]; if (!enchantment || !level) continue; if (enchantment.cropSpecific && !this.crops.includes(enchantment.cropSpecific)) continue; for (const crop of this.crops) { let fortune = getFortuneFromEnchant(level, enchantment, this.options, crop); const cropStat = CROP_INFO[crop]?.fortuneType; if (cropStat) { fortune += getStatFromEnchant(level, enchantment, cropStat, this.options, crop); } if (fortune > 0) { this.fortuneBreakdown[enchantment.name] = fortune; sum += fortune; } } } this.fortune = sum; return sum; } getCounter() { const counter = +(this.item?.attributes?.mined_crops ?? 0); return counter && !isNaN(counter) ? counter : undefined; } getCultivating() { const cultivating = +(this.item?.attributes?.farmed_cultivating ?? 0); return cultivating && !isNaN(cultivating) ? cultivating : undefined; } getToolLevel() { const level = +(this.item?.attributes?.levelable_lvl ?? 1); return level && !isNaN(level) ? level : 1; } getXp() { const xp = +(this.item?.attributes?.levelable_exp ?? 0); return xp && !isNaN(xp) ? xp : 0; } getOverclocks() { const overclocks = +(this.item?.attributes?.levelable_overclocks ?? 0); return overclocks && !isNaN(overclocks) ? overclocks : 0; } getCultivatingLevel() { return this.item.enchantments?.cultivating ?? 0; } get farmed() { return this.counter ?? this.cultivating ?? 0; } isUsed() { if (this.farmed > 0) return true; if (this.getCultivatingLevel() > 0) return true; return false; } supportsCultivating() { return this.type ? (FARMING_ENCHANTS.cultivating?.appliesTo.includes(this.type) ?? false) : false; } isMissingDedication() { if (this.crops.length === 0) return false; return (this.item?.enchantments?.dedication && this.crops.some((crop) => (this.options?.milestones?.[crop] ?? 0) <= 0)); } static isValid(item) { if (item instanceof FarmingTool) return true; return FARMING_TOOLS[item.skyblockId] !== undefined; } static fromArray(items, options) { return items .filter((item) => FarmingTool.isValid(item)) .map((item) => new FarmingTool(item, options)) .sort((a, b) => b.fortune - a.fortune); } static fakeItem(info, options) { const fake = { name: info.name, skyblockId: info.skyblockId, uuid: crypto.randomUUID(), lore: ['This is a fake item used for upgrade calculations!'], attributes: {}, enchantments: {}, }; if (!FarmingTool.isValid(fake)) return undefined; return new FarmingTool(fake, options); } } //# sourceMappingURL=farmingtool.js.map