farming-weight
Version:
Tools for calculating farming weight and fortune in Hypixel Skyblock
252 lines • 11.3 kB
JavaScript
import { getChipInputLevel, getChipLevel, getChipRarity, normalizeChipLevels } from '../constants/chips.js';
import { CROP_INFO, Crop, MAX_CROP_FORTUNE } from '../constants/crops.js';
import { Rarity, REFORGES } from '../constants/reforges.js';
import { MATCHING_SPECIAL_CROP, SPECIAL_CROP_INFO } from '../constants/specialcrops.js';
import { Stat } from '../constants/stats.js';
import { calculateAverageSpecialCrops } from '../crops/special.js';
import { produceAddedDrops, resolveDropEffects } from '../effects/resolver.js';
import { BEST_FARMING_TOOLS } from '../items/tools.js';
const DEFAULT_RNG_TAGS = ['overbloom', 'rare-crop'];
function tagSet(tags) {
return new Set(tags ?? DEFAULT_RNG_TAGS);
}
function getEffectiveFortune(opts) {
const base = opts.farmingFortune ?? MAX_CROP_FORTUNE[opts.crop] ?? 0;
let fortune = base + 100;
if (!opts.bountiful && !opts.farmingFortune) {
const maxRarity = BEST_FARMING_TOOLS[opts.crop]?.maxRarity ?? Rarity.Mythic;
const bountifulFortune = REFORGES.bountiful?.tiers[maxRarity]?.stats?.[Stat.FarmingFortune] ?? 0;
const blessedFortune = REFORGES.blessed?.tiers[maxRarity]?.stats?.[Stat.FarmingFortune] ?? 0;
fortune += blessedFortune - bountifulFortune;
}
return fortune;
}
function getCropInfo(crop) {
return CROP_INFO[crop] ?? {};
}
function buildBaseRngCandidates(cropInfo, env, blocksBroken) {
const out = [];
const inSeason = env.harvestFeast && env.inSeason;
for (const drop of cropInfo.rng ?? []) {
if (drop.only === 'harvestFeast' && !inSeason)
continue;
const tags = tagSet(drop.tags);
const kind = drop.dropKind ?? 'rng';
for (const [item, count] of Object.entries(drop.drops)) {
out.push({
itemId: item,
output: drop.output ?? 'rng',
baseAmount: drop.chance * blocksBroken * count,
tags,
dropKind: kind,
});
}
}
return out;
}
function appendAddedDropCandidates(candidates, effects, env, blocksBroken) {
for (const { source, payload } of produceAddedDrops(effects, env)) {
const baseAmount = payload.baseAmount !== undefined
? payload.baseAmount * blocksBroken
: payload.chance !== undefined
? payload.chance * blocksBroken
: 0;
if (baseAmount <= 0)
continue;
candidates.push({
itemId: payload.itemId,
output: payload.output ?? 'rng',
baseAmount,
tags: tagSet(payload.tags),
dropKind: payload.dropKind ?? 'rare',
fromAddDrop: true,
source,
});
}
}
function aggregateEffectsBreakdown(target, applied) {
for (const entry of applied) {
// Only `add-rare-pct` contributes additively to the breakdown
if (entry.op !== 'add-rare-pct')
continue;
const prev = target[entry.source] ?? 0;
if (entry.amount > prev)
target[entry.source] = entry.amount;
}
}
/**
* Run the new effect-driven detailed-drops calculation.
*/
export function calculateDetailedDropsFromEffects(options) {
const calcOptions = {
...options,
chips: normalizeChipLevels(options.chips ?? {}),
};
const { crop, blocksBroken, env, effects } = calcOptions;
const result = {
npcPrice: 0,
collection: 0,
npcCoins: 0,
fortune: 0,
blocksBroken,
coinSources: {},
otherCollection: {},
items: {},
currencies: {},
specialCropBonus: 0,
specialCropBonusBreakdown: {},
appliedEffects: {},
effectsBreakdown: {},
};
result.fortune = calcOptions.farmingFortune ?? MAX_CROP_FORTUNE[crop] ?? 0;
const fortune = getEffectiveFortune(calcOptions);
if (fortune <= 0 || blocksBroken < 0)
return result;
const cropInfo = getCropInfo(crop);
const { drops, npc, breaks = 1, replenish = false } = cropInfo;
result.npcPrice = npc;
if (!drops)
return result;
const baseDrops = blocksBroken * drops * (fortune * 0.01);
result.otherCollection['Normal'] = Math.round(baseDrops);
if (calcOptions.bountiful) {
result.coinSources['Bountiful'] = Math.round(baseDrops * 0.2);
}
if (calcOptions.mooshroom) {
const mushroomDrops = Math.round(blocksBroken * breaks);
result.coinSources['Mooshroom'] = mushroomDrops * CROP_INFO[Crop.Mushroom].npc;
result.otherCollection['Mushroom'] = mushroomDrops;
result.items[Crop.Mushroom] = mushroomDrops;
}
const armorPieces = Math.min(Math.max(calcOptions.armorPieces ?? 4, 0), 4);
const baseSpecialCrops = calculateAverageSpecialCrops(blocksBroken, crop, armorPieces, 1);
result.otherCollection[baseSpecialCrops.type] = baseSpecialCrops.amount;
result.items[baseSpecialCrops.id] = baseSpecialCrops.amount;
result.coinSources[baseSpecialCrops.type] = baseSpecialCrops.npc;
if (replenish) {
result.coinSources['Collection'] = Math.round((baseDrops - blocksBroken * breaks) * npc);
result.otherCollection['Replenish'] = -Math.round(blocksBroken * breaks);
result.collection = Math.round(baseDrops);
result.items[crop] = Math.round(baseDrops - blocksBroken * breaks);
}
else {
result.coinSources['Collection'] = Math.round(baseDrops * npc);
result.collection = Math.round(baseDrops);
result.items[crop] = Math.round(baseDrops);
}
if (crop === Crop.Wheat) {
const seedsResult = calculateDetailedDropsFromEffects({
...calcOptions,
crop: Crop.Seeds,
maxTool: false,
mooshroom: false,
env: { ...env, crop: Crop.Seeds },
});
const seedCollection = seedsResult.collection - blocksBroken;
result.otherCollection['Seeds'] = seedCollection;
result.items[Crop.Seeds] = seedCollection;
result.coinSources['Seeds'] = seedCollection * seedsResult.npcPrice;
if (calcOptions.bountiful) {
result.coinSources['Bountiful (Seeds)'] = seedsResult.coinSources['Bountiful'] ?? 0;
}
}
if (calcOptions.maxTool) {
let multiplier = 1;
if (calcOptions.chips) {
const level = getChipLevel(getChipInputLevel(calcOptions.chips, 'mechamind'));
if (level > 0) {
const rarity = getChipRarity(level);
let perLevel = 0.015;
if (rarity === Rarity.Epic)
perLevel = 0.02;
else if (rarity === Rarity.Legendary)
perLevel = 0.025;
multiplier = 1 + level * perLevel;
}
}
const toolXpFactor = cropInfo.toolXpFactor ?? 1;
const capsules = Math.floor(((result.collection + (result.otherCollection['Seeds'] ?? 0)) * multiplier) / toolXpFactor / 200_000);
if (capsules > 0) {
result.items['TOOL_EXP_CAPSULE'] = capsules;
result.coinSources['Tool Exp Capsule'] = capsules * 100_000;
result.otherCollection['Tool Exp Capsule'] = capsules;
}
}
// Build candidate RNG drops: built-in `rng:` entries + `add-drop` payloads.
const candidates = buildBaseRngCandidates(cropInfo, env, blocksBroken);
appendAddedDropCandidates(candidates, effects, env, blocksBroken);
for (const candidate of candidates) {
const ctx = {
env,
crop,
dropKind: candidate.dropKind,
itemId: candidate.itemId,
tags: candidate.tags,
fromAddDrop: candidate.fromAddDrop,
};
const { addRarePct, mulRare, mulDrop, applied } = resolveDropEffects(effects, ctx);
const rareMultiplier = (1 + addRarePct / 100) * mulRare * mulDrop;
const finalAmount = candidate.baseAmount * rareMultiplier;
if (finalAmount <= 0)
continue;
if (candidate.output === 'collection') {
result.collection += finalAmount;
result.items[candidate.itemId] = (result.items[candidate.itemId] ?? 0) + finalAmount;
result.otherCollection[candidate.source ?? candidate.itemId] =
(result.otherCollection[candidate.source ?? candidate.itemId] ?? 0) + finalAmount;
const itemNpc = CROP_INFO[candidate.itemId]?.npc ?? 0;
if (itemNpc > 0) {
result.coinSources[candidate.source ?? candidate.itemId] =
(result.coinSources[candidate.source ?? candidate.itemId] ?? 0) + finalAmount * itemNpc;
}
if (applied.length > 0) {
result.appliedEffects[candidate.itemId] = (result.appliedEffects[candidate.itemId] ?? []).concat(applied);
aggregateEffectsBreakdown(result.effectsBreakdown, applied);
}
continue;
}
if (candidate.output === 'currency') {
result.currencies[candidate.itemId] = (result.currencies[candidate.itemId] ?? 0) + finalAmount;
if (applied.length > 0) {
result.appliedEffects[candidate.itemId] = (result.appliedEffects[candidate.itemId] ?? []).concat(applied);
aggregateEffectsBreakdown(result.effectsBreakdown, applied);
}
continue;
}
result.rngItems ??= {};
result.rngItems[candidate.itemId] = (result.rngItems[candidate.itemId] ?? 0) + finalAmount;
if (applied.length > 0) {
result.appliedEffects[candidate.itemId] = (result.appliedEffects[candidate.itemId] ?? []).concat(applied);
aggregateEffectsBreakdown(result.effectsBreakdown, applied);
}
}
// Special crops: re-scale by the multiplicative bonus produced by effects whose
// scope matches a special-crop drop. We pick a representative special crop drop
// for this `crop` (the matching SpecialCrop tier) and feed it into the resolver.
const specialCrop = MATCHING_SPECIAL_CROP[crop];
if (specialCrop) {
const specialCtx = {
env,
crop,
dropKind: 'special-crop',
itemId: SPECIAL_CROP_INFO[specialCrop].id,
specialCropType: specialCrop,
tags: new Set(['overbloom', 'rare-crop', 'special-crop']),
};
const { addRarePct, mulRare, mulDrop, applied } = resolveDropEffects(effects, specialCtx);
const totalMultiplier = (1 + addRarePct / 100) * mulRare * mulDrop;
if (totalMultiplier !== 1 && totalMultiplier > 0) {
const newAmount = calculateAverageSpecialCrops(blocksBroken, crop, armorPieces, totalMultiplier);
result.otherCollection[specialCrop] = newAmount.amount;
result.items[SPECIAL_CROP_INFO[specialCrop].id] = newAmount.amount;
result.coinSources[specialCrop] = newAmount.npc;
if (applied.length > 0) {
result.appliedEffects[SPECIAL_CROP_INFO[specialCrop].id] = applied;
aggregateEffectsBreakdown(result.effectsBreakdown, applied);
}
}
}
result.npcCoins = Object.values(result.coinSources).reduce((a, b) => a + b, 0);
return result;
}
//# sourceMappingURL=ratecalc-effects.js.map