farming-weight
Version:
Tools for calculating farming weight and fortune in Hypixel Skyblock
575 lines • 22.1 kB
JavaScript
import { FARMING_ENCHANTS } from '../constants/enchants.js';
import { ReforgeTarget } from '../constants/reforges.js';
import { Skill } from '../constants/skills.js';
import { MATCHING_SPECIAL_CROP } from '../constants/specialcrops.js';
import { Stat } from '../constants/stats.js';
import { getQueryStats, } from '../constants/upgrades.js';
import { calculateAverageSpecialCrops } from '../crops/special.js';
import { ARMOR_SET_BONUS, FARMING_ARMOR_INFO, GEAR_SLOTS, GearSlot, } from '../items/armor.js';
import { FARMING_EQUIPMENT_INFO } from '../items/equipment.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 { getSourceProgress } from '../upgrades/getsourceprogress.js';
import { ARMOR_SET_FORTUNE_SOURCES } from '../upgrades/sources/armorsetsources.js';
import { GEAR_FORTUNE_SOURCES } from '../upgrades/sources/gearsources.js';
import { getLastItemUpgradeableTo, getSelfFortuneUpgrade, getUpgradeableRarityUpgrade } from '../upgrades/upgrades.js';
import { filterAndSortUpgrades } from '../upgrades/upgradeutils.js';
import { getFortuneFromEnchant, getStatFromEnchant } from '../util/enchants.js';
import { getGemStat, getPeridotFortune } from '../util/gems.js';
import { FarmingEquipment } from './farmingequipment.js';
import { UpgradeableBase } from './upgradeablebase.js';
export class ArmorSet {
get armor() {
return [this.helmet ?? null, this.chestplate ?? null, this.leggings ?? null, this.boots ?? null];
}
get equipment() {
return [this.necklace ?? null, this.cloak ?? null, this.belt ?? null, this.gloves ?? null];
}
get fortune() {
return this.armorFortune + this.equipmentFortune;
}
constructor(armor, equipment, options) {
this.setBonuses = [];
this.equipmentSetBonuses = [];
this.options = options;
if (options) {
for (const piece of armor) {
piece.setOptions(options);
}
for (const piece of equipment ?? []) {
piece.setOptions(options);
}
}
this.setArmor(armor);
if (equipment) {
this.setEquipment(equipment);
}
else {
this.recalculateFamilies();
}
}
setArmor(armor) {
armor.sort((a, b) => b.potential - a.potential);
this.pieces = armor;
this.helmet = armor.find((a) => a.slot === GearSlot.Helmet);
this.chestplate = armor.find((a) => a.slot === GearSlot.Chestplate);
this.leggings = armor.find((a) => a.slot === GearSlot.Leggings);
this.boots = armor.find((a) => a.slot === GearSlot.Boots);
this.recalculateFamilies();
}
setEquipment(equipment) {
equipment.sort((a, b) => b.fortune - a.fortune);
this.equipmentPieces = equipment;
this.necklace = equipment.find((a) => a.slot === GearSlot.Necklace);
this.cloak = equipment.find((a) => a.slot === GearSlot.Cloak);
this.belt = equipment.find((a) => a.slot === GearSlot.Belt);
this.gloves = equipment.find((a) => a.slot === GearSlot.Gloves);
this.recalculateFamilies();
}
setOptions(options) {
for (const piece of this.pieces) {
piece.setOptions(options);
}
for (const piece of this.equipmentPieces) {
piece.setOptions(options);
}
if (!this.options) {
this.resetChosenPieces();
}
this.getFortuneBreakdown(true);
this.options = options;
}
resetChosenPieces() {
this.setArmor(this.pieces);
this.setEquipment(this.equipmentPieces);
}
updateArmorSlot(piece) {
// Update the pieces array
const idx = this.pieces.findIndex((p) => p.item.uuid === piece.item.uuid || p.item.skyblockId === piece.item.skyblockId);
if (idx >= 0) {
this.pieces[idx] = piece;
}
else {
this.pieces.push(piece);
}
// Update the specific slot
switch (piece.slot) {
case GearSlot.Helmet:
this.helmet = piece;
break;
case GearSlot.Chestplate:
this.chestplate = piece;
break;
case GearSlot.Leggings:
this.leggings = piece;
break;
case GearSlot.Boots:
this.boots = piece;
break;
}
this.recalculateFamilies();
}
updateEquipmentSlot(piece) {
// Update the equipmentPieces array
const idx = this.equipmentPieces.findIndex((p) => p.item.uuid === piece.item.uuid || p.item.skyblockId === piece.item.skyblockId);
if (idx >= 0) {
this.equipmentPieces[idx] = piece;
}
else {
this.equipmentPieces.push(piece);
}
// Update the specific slot
switch (piece.slot) {
case GearSlot.Necklace:
this.necklace = piece;
break;
case GearSlot.Cloak:
this.cloak = piece;
break;
case GearSlot.Belt:
this.belt = piece;
break;
case GearSlot.Gloves:
this.gloves = piece;
break;
}
this.recalculateFamilies();
}
getPiece(slot) {
switch (slot) {
case GearSlot.Helmet:
return this.helmet;
case GearSlot.Chestplate:
return this.chestplate;
case GearSlot.Leggings:
return this.leggings;
case GearSlot.Boots:
return this.boots;
case GearSlot.Necklace:
return this.necklace;
case GearSlot.Cloak:
return this.cloak;
case GearSlot.Belt:
return this.belt;
case GearSlot.Gloves:
return this.gloves;
default:
return;
}
}
getStartingPiece(slot) {
const info = GEAR_SLOTS[slot];
return info.target === ReforgeTarget.Armor
? FarmingArmor.fakeItem(FARMING_ARMOR_INFO[info.startingItem])
: FarmingEquipment.fakeItem(FARMING_EQUIPMENT_INFO[info.startingItem]);
}
setPiece(armor) {
if (armor instanceof FarmingArmor) {
switch (armor.slot) {
case GearSlot.Helmet:
this.helmet = armor;
break;
case GearSlot.Chestplate:
this.chestplate = armor;
break;
case GearSlot.Leggings:
this.leggings = armor;
break;
case GearSlot.Boots:
this.boots = armor;
break;
}
}
else if (armor instanceof FarmingEquipment) {
switch (armor.slot) {
case GearSlot.Necklace:
this.necklace = armor;
break;
case GearSlot.Cloak:
this.cloak = armor;
break;
case GearSlot.Belt:
this.belt = armor;
break;
case GearSlot.Gloves:
this.gloves = armor;
break;
}
}
this.getFortuneBreakdown(true);
}
recalculateFamilies() {
this.setBonuses = ArmorSet.getSetBonusFrom(this.armor ?? []);
this.equipmentSetBonuses = ArmorSet.getSetBonusFrom(this.equipment ?? []);
this.getFortuneBreakdown();
}
static getSetBonusFrom(armor) {
const families = new Map();
const result = [];
for (const piece of armor) {
if (!piece?.info.family)
continue;
families.set(piece.info.family, (families.get(piece.info.family) ?? 0) + 1);
}
for (const [family, count] of families.entries()) {
if (count < 2)
continue;
const bonus = ARMOR_SET_BONUS[family];
if (!bonus)
continue;
result.push({
count: count,
from: armor.filter((a) => a?.info.family === family).map((a) => a?.slot),
bonus: bonus,
special: bonus.special,
});
}
return result;
}
getStat(stat, crop) {
let sum = 0;
// Armor fortune
for (const piece of this.armor) {
if (!piece)
continue;
sum += piece.getStat(stat, crop);
}
// Armor set bonuses
for (const { bonus, count } of this.setBonuses) {
if (count < 2 || count > 4)
continue;
sum += bonus.stats?.[count]?.[stat] ?? 0;
}
// Equipment fortune
for (const piece of this.equipment) {
if (!piece)
continue;
sum += piece.getStat(stat, crop);
}
// Equipment set bonuses
for (const { bonus, count } of this.equipmentSetBonuses) {
if (count < 2 || count > 4)
continue;
sum += bonus.stats?.[count]?.[stat] ?? 0;
}
return sum;
}
/**
* Returns the declarative `Effect[]` for the entire armor set: every armor
* piece's effects, every equipment piece's effects, and the armor / equipment
* set bonuses keyed by current piece count.
*/
getEffects(env) {
const effects = [];
for (const piece of this.armor) {
if (!piece)
continue;
effects.push(...piece.getEffects(env));
}
for (const { bonus, count } of this.setBonuses) {
if (count < 2 || count > 4)
continue;
const stats = bonus.stats?.[count];
if (!stats)
continue;
effects.push(...statsToEffects(stats, `${bonus.name} (${count}-piece)`));
}
for (const piece of this.equipment) {
if (!piece)
continue;
effects.push(...piece.getEffects(env));
}
for (const { bonus, count } of this.equipmentSetBonuses) {
if (count < 2 || count > 4)
continue;
const stats = bonus.stats?.[count];
if (!stats)
continue;
effects.push(...statsToEffects(stats, `${bonus.name} (${count}-piece)`));
}
return effects;
}
getFortuneBreakdown(reloadFamilies = false) {
if (reloadFamilies) {
this.recalculateFamilies();
}
let sum = 0;
const breakdown = {};
// Armor fortune
for (const piece of this.armor) {
if (!piece)
continue;
const fortune = piece.fortune;
if (fortune > 0) {
breakdown[piece.item.name ?? ''] = fortune;
sum += fortune;
}
}
// Armor set bonuses
for (const { bonus, count } of this.setBonuses) {
if (count < 2 || count > 4)
continue;
const fortune = bonus.stats?.[count]?.[Stat.FarmingFortune] ?? 0;
if (fortune > 0) {
breakdown[bonus.name] = fortune;
sum += fortune;
}
}
this.armorFortune = sum;
// Equipment fortune
let equipmentSum = 0;
for (const piece of this.equipment) {
if (!piece)
continue;
const fortune = piece.fortune;
if (fortune > 0) {
breakdown[piece.item.name ?? ''] = fortune;
equipmentSum += fortune;
}
}
// Equipment set bonuses
for (const { bonus, count } of this.equipmentSetBonuses) {
if (count < 2 || count > 4)
continue;
const fortune = bonus.stats?.[count]?.[Stat.FarmingFortune] ?? 0;
if (fortune > 0) {
breakdown[bonus.name] = fortune;
equipmentSum += fortune;
}
}
this.equipmentFortune = equipmentSum;
return breakdown;
}
specialDropsCalc(blocksBroken, crop) {
const count = this.specialDropsCount(crop);
if (count === 0)
return null;
return calculateAverageSpecialCrops(blocksBroken, crop, count);
}
specialDropsCount(crop) {
const special = MATCHING_SPECIAL_CROP[crop];
const applicableBonuses = this.setBonuses.filter((b) => b.special?.includes(special));
if (applicableBonuses.length === 0)
return 0;
// Mixed armor families should use the best active same-family tier.
// For example, 2 Fermento + 2 Helianthus is not a 4-piece Feast bonus;
// it is two separate 2-piece bonuses, so the special-crop rate uses 2.
return Math.max(...applicableBonuses.map((bonus) => bonus.count));
}
getProgress(stats, zeroed = false) {
return getSourceProgress(this, ARMOR_SET_FORTUNE_SOURCES, zeroed, stats);
}
getUpgrades(options) {
const hasExplicitStats = (options?.stats?.length ?? 0) > 0 || options?.stat !== undefined;
const stats = hasExplicitStats ? getQueryStats(options) : undefined;
const upgrades = getSourceProgress(this, ARMOR_SET_FORTUNE_SOURCES, false, stats).flatMap((source) => source.upgrades ?? []);
return filterAndSortUpgrades(upgrades, options);
}
getPieceProgress(slot) {
let piece = this.getPiece(slot);
if (!piece) {
piece = this.getStartingPiece(slot);
return piece?.getProgress(undefined, true) ?? [];
}
return piece.getProgress() ?? [];
}
get slots() {
return {
[GearSlot.Helmet]: this.helmet,
[GearSlot.Chestplate]: this.chestplate,
[GearSlot.Leggings]: this.leggings,
[GearSlot.Boots]: this.boots,
[GearSlot.Necklace]: this.necklace,
[GearSlot.Cloak]: this.cloak,
[GearSlot.Belt]: this.belt,
[GearSlot.Gloves]: this.gloves,
};
}
get slotOptions() {
return {
[GearSlot.Helmet]: this.pieces.filter((a) => a.slot === GearSlot.Helmet),
[GearSlot.Chestplate]: this.pieces.filter((a) => a.slot === GearSlot.Chestplate),
[GearSlot.Leggings]: this.pieces.filter((a) => a.slot === GearSlot.Leggings),
[GearSlot.Boots]: this.pieces.filter((a) => a.slot === GearSlot.Boots),
[GearSlot.Necklace]: this.equipmentPieces.filter((a) => a.slot === GearSlot.Necklace),
[GearSlot.Cloak]: this.equipmentPieces.filter((a) => a.slot === GearSlot.Cloak),
[GearSlot.Belt]: this.equipmentPieces.filter((a) => a.slot === GearSlot.Belt),
[GearSlot.Gloves]: this.equipmentPieces.filter((a) => a.slot === GearSlot.Gloves),
};
}
}
export class FarmingArmor extends UpgradeableBase {
get type() {
return ReforgeTarget.Armor;
}
// Backwards compatibility
get armor() {
return this.info;
}
get slot() {
return this.info.slot;
}
get potential() {
if (!this.info.family) {
return this.fortune;
}
// Add the set bonus potential to the fortune
return this.fortune + (ARMOR_SET_BONUS[this.info.family]?.piecePotential?.[Stat.FarmingFortune] ?? 0);
}
constructor(item, options) {
super({ item, options, items: FARMING_ARMOR_INFO });
this.getFortune();
}
setOptions(options) {
this.options = options;
this.fortune = this.getFortune();
}
getStat(stat, crop) {
let sum = 0;
// Base stats
sum += this.info.baseStats?.[stat] ?? 0;
// Per farming level stats like Rancher's Boots
if (this.info.perLevelStats?.skill === Skill.Farming && this.options?.farmingLevel) {
sum += (this.info.perLevelStats?.stats[stat] ?? 0) * this.options.farmingLevel;
}
// Reforge stats
sum += this.reforgeStats?.stats?.[stat] ?? 0;
// Gems
sum += getGemStat(this.item, stat, this.rarity);
// 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 || enchantment.cropSpecific)
continue;
sum += getStatFromEnchant(level, enchantment, stat, this.options, crop);
}
return sum;
}
/**
* Returns the declarative `Effect[]` representation of every contribution
* this armor piece makes: base stats, per-farming-level stats, reforge,
* gems, and enchants. Set bonuses are handled at the `ArmorSet` level (see
* {@link ArmorSet.getEffects}).
*/
getEffects(env) {
const sourceName = this.item.name ?? this.info.name;
const effects = [];
effects.push(...statsToEffects(this.info.baseStats, sourceName));
if (this.info.perLevelStats?.skill === Skill.Farming && this.options?.farmingLevel) {
const perLevel = {};
for (const [statKey, value] of Object.entries(this.info.perLevelStats.stats)) {
if (value)
perLevel[statKey] = value * this.options.farmingLevel;
}
effects.push(...statsToEffects(perLevel, `${sourceName} (Farming Level)`));
}
if (this.reforge && this.item.attributes?.modifier) {
effects.push(...reforgeEffects(this.item.attributes.modifier, this.rarity, `${sourceName} (${this.reforge.name})`));
}
effects.push(...gemEffects(this.item, this.rarity, `${sourceName} (Gems)`));
for (const [enchantId, level] of Object.entries(this.item.enchantments ?? {})) {
if (!level)
continue;
effects.push(...enchantEffects(enchantId, level, env, this.options ?? {}));
}
return effects;
}
getFortune() {
this.fortuneBreakdown = {};
let sum = 0;
// Base fortune
const base = this.info.baseStats?.[Stat.FarmingFortune] ?? 0;
if (base > 0) {
this.fortuneBreakdown['Base Stats'] = base;
sum += base;
}
// Per farming level stats like Rancher's Boots
if (this.info.perLevelStats?.skill === Skill.Farming && this.options?.farmingLevel) {
const perLevel = this.info.perLevelStats?.stats[Stat.FarmingFortune] ?? 0;
if (perLevel > 0) {
this.fortuneBreakdown['Farming Level'] = perLevel * this.options.farmingLevel;
sum += perLevel * this.options.farmingLevel;
}
}
// Reforge stats
const reforge = this.reforgeStats?.stats?.[Stat.FarmingFortune] ?? 0;
if (reforge > 0) {
this.fortuneBreakdown[this.reforge?.name ?? 'Reforge'] = reforge;
sum += reforge;
}
// 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 || enchantment.cropSpecific)
continue;
const fortune = getFortuneFromEnchant(level, enchantment, this.options);
if (fortune > 0) {
this.fortuneBreakdown[enchantment.name] = fortune;
sum += fortune;
}
}
this.fortune = sum;
return sum;
}
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, GEAR_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);
}
getItemUpgrade() {
return this.info.upgrade;
}
getLastItemUpgrade() {
return getLastItemUpgradeableTo(this, FARMING_ARMOR_INFO);
}
getProgress(stats, zeroed = false) {
return getSourceProgress(this, GEAR_FORTUNE_SOURCES, zeroed, stats);
}
static isValid(item) {
return FARMING_ARMOR_INFO[item.skyblockId] !== undefined;
}
static fromArray(items, options) {
return items
.filter((item) => FarmingArmor.isValid(item))
.map((item) => new FarmingArmor(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 (!FarmingArmor.isValid(fake))
return undefined;
return new FarmingArmor(fake, options);
}
}
//# sourceMappingURL=farmingarmor.js.map