UNPKG

farming-weight

Version:

Tools for calculating farming weight and fortune in Hypixel Skyblock

443 lines 17.5 kB
import { getChipInputLevel, getChipLevel, getChipTempMultiplierPerLevel } from '../constants/chips.js'; import { RARITY_COLORS, Rarity } from '../constants/reforges.js'; import { getStatValue, Stat } from '../constants/stats.js'; import { getQueryStats, UpgradeAction, UpgradeCategory, } from '../constants/upgrades.js'; import { FARMING_PET_ITEMS, FARMING_PETS, FarmingPetStatType, PET_LEVELS, PET_RARITY_OFFSETS, } from '../items/pets.js'; import { statsToEffects } from '../items/sources/effects-util.js'; import { getRarityFromLore } from '../util/itemstats.js'; export function createFarmingPet(pet) { return new FarmingPet(pet); } function getPetItemId(item) { return Object.entries(FARMING_PET_ITEMS).find(([, value]) => value === item)?.[0]; } function improvesRequestedStat(upgrade, stats) { return stats.some((stat) => (upgrade.stats?.[stat] ?? 0) > 0); } export class FarmingPet { constructor(pet, options) { this.options = options; this.pet = pet; if (!this.pet.uuid) { // Generate a UUID for the pet this.pet.uuid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } this.info = FARMING_PETS[pet.type]; if (!this.info) { throw new Error(`Invalid farming pet type: ${pet.type}`); } this.type = pet.type; this.rarity = getRarityFromLore([pet.tier ?? '']) ?? Rarity.Common; this.level = this.getLevel(); this.item = pet.heldItem ? FARMING_PET_ITEMS[pet.heldItem] : undefined; this.fortune = this.getFortune(); } setOptions(options) { this.options = options; this.fortune = this.getFortune(); } computeFortune(stat = Stat.FarmingFortune, player) { let fortune = 0; const breakdown = {}; // Base stats const baseStat = this.info.stats?.[stat]; const stats = getStatValue(baseStat, this); if (stats) { fortune += stats; breakdown[baseStat?.name ?? 'Base Stats'] = stats; } // Per level stats const perLevelStats = this.info.perLevelStats?.[stat]; if (perLevelStats) { const amount = getStatValue(perLevelStats, this) * this.level; fortune += amount; breakdown[perLevelStats.name ?? 'Unknown'] = amount; } // Per rarity fortune stats const perRarityStats = this.info.perRarityLevelStats?.[this.rarity]?.[stat]; if (perRarityStats) { const amount = getStatValue(perRarityStats, this) * this.level; fortune += amount; breakdown[perRarityStats.name ?? 'Rarity Stat'] = amount; } // Pet abilities if (this.info.abilities) { const hyperLevel = getChipLevel(getChipInputLevel(this.options?.chips, 'hypercharge')); const perLevel = getChipTempMultiplierPerLevel('hypercharge', hyperLevel); const hyperchargeMultiplier = 1 + perLevel * hyperLevel; for (const ability of this.info.abilities) { if (ability.exists && !ability.exists({ player, options: this.options ?? {} }, this)) { continue; } const stats = ability.computed({ player, options: this.options ?? {} }, this); const fortuneStat = stats[stat]; let value = getStatValue(fortuneStat, this.options); if (!value || !fortuneStat) continue; if (ability.temporary) { value *= hyperchargeMultiplier; } fortune += value; breakdown[fortuneStat.name ?? ability.name] = value; } } // Pet item stats if (this.item) { const fortuneStat = this.item.stats?.[stat]; const value = getStatValue(fortuneStat, this.options); if (value && fortuneStat) { fortune += value; breakdown[this.item.name] = value; } } return { fortune, breakdown }; } getFortune(stat = Stat.FarmingFortune, player) { const { fortune, breakdown } = this.computeFortune(stat, player); if (stat === Stat.FarmingFortune) { this.breakdown = breakdown; this.fortune = fortune; } return fortune; } /** * Returns the declarative `Effect[]` representation of every per-stat * contribution this pet makes (base, per-level, per-rarity-level, abilities, * pet item) */ getEffects(_env, player) { const sourceName = this.info.name ?? this.type; const stats = {}; for (const stat of Object.values(Stat)) { const { fortune } = this.computeFortune(stat, player); if (fortune) stats[stat] = fortune; } return statsToEffects(stats, sourceName); } getFullBreakdown(player) { const full = {}; let baseFortune = 0; for (const stat of Object.values(Stat)) { const { fortune, breakdown } = this.computeFortune(stat, player); if (!fortune) continue; // Track base farming fortune for late context if (stat === Stat.FarmingFortune) { baseFortune = fortune; } for (const [name, value] of Object.entries(breakdown)) { if (value === 0) continue; const existing = full[name]; if (existing && existing.stat === stat) { existing.value += value; } else { full[name] = { value, stat }; } } } // Include late-phase stats when player is available // Use player's baseFortune for late calcs that depend on total fortune (e.g., Trample) if (player) { const lateContext = { player, baseFortune: player.baseFortune ?? baseFortune, stat: Stat.FarmingFortune, }; const lateResult = this.getLateStats(lateContext); if (lateResult.breakdown) { for (const [name, entry] of Object.entries(lateResult.breakdown)) { full[name] = entry; } } } return full; } /** * Get late-phase stats for abilities that depend on total fortune. * Called after all base stats have been computed. */ getLateStats(ctx) { const result = {}; if (!this.info.abilities) { return result; } for (const ability of this.info.abilities) { if (!ability.lateComputed) continue; // Check if ability exists for this pet if (ability.exists) { const player = ctx.player; if (!ability.exists({ player, options: this.options ?? {} }, this)) { continue; } } const lateResult = ability.lateComputed(ctx, this); // Merge additive values if (lateResult.additive !== undefined) { result.additive = (result.additive ?? 0) + lateResult.additive; } // Combine multipliers (multiplicative stacking) if (lateResult.multiplier !== undefined) { result.multiplier = (result.multiplier ?? 1) * lateResult.multiplier; } // Merge breakdown entries if (lateResult.breakdown) { result.breakdown = { ...result.breakdown, ...lateResult.breakdown }; } } return result; } getFormattedName() { return '[' + this.level + '] ' + RARITY_COLORS[this.rarity] + this.info.name; } getLevel() { const offset = PET_RARITY_OFFSETS[this.rarity] ?? 0; const maxLevel = this.info.maxLevel ?? 100; let xp = this.pet.exp ?? 0; for (let i = offset; i < Math.min(PET_LEVELS.length, maxLevel + offset); i++) { const level = PET_LEVELS[i]; if (level === undefined) break; if (xp < level) { return i + 1 - offset; } xp -= level; } return maxLevel; } getXpForLevel(level) { const offset = PET_RARITY_OFFSETS[this.rarity] ?? 0; const maxLevel = this.info.maxLevel ?? 100; const targetLevel = Math.max(1, Math.min(level, maxLevel)); let xp = 0; for (let i = offset; i < offset + targetLevel - 1; i++) { xp += PET_LEVELS[i] ?? 0; } return xp; } withChanges(changes) { return new FarmingPet({ ...this.pet, ...changes }, this.options); } getStatTotals(stats, player) { const totals = {}; for (const stat of stats) { const value = this.getFortune(stat, player); if (value !== 0) totals[stat] = value; } return totals; } getBreakdownProgress(maxPet, stats, player) { const progress = new Map(); for (const stat of stats) { const currentBreakdown = this.computeFortune(stat, player).breakdown; const maxBreakdown = maxPet.computeFortune(stat, player).breakdown; const sourceNames = new Set([...Object.keys(currentBreakdown), ...Object.keys(maxBreakdown)]); for (const name of sourceNames) { const current = currentBreakdown[name] ?? 0; const max = Math.max(maxBreakdown[name] ?? 0, current); if (current === 0 && max === 0) continue; const entry = progress.get(name) ?? { name, current: 0, max: 0, ratio: 0, stats: {}, }; entry.stats ??= {}; entry.stats[stat] = { current, max, ratio: Math.min(max === 0 ? 0 : current / max, 1), }; if (stat === Stat.FarmingFortune || entry.current === 0) { entry.current = current; entry.max = max; entry.ratio = Math.min(max === 0 ? 0 : current / max, 1); } progress.set(name, entry); } } return [...progress.values()].sort((a, b) => (b.current ?? 0) - (a.current ?? 0)); } getDeltaStats(next, player) { const deltaStats = {}; for (const stat of Object.values(Stat)) { const delta = next.getFortune(stat, player) - this.getFortune(stat, player); if (delta !== 0) deltaStats[stat] = +delta.toFixed(4); } return deltaStats; } getProgressItem() { return { id: 0, count: 1, skyblockId: this.type, uuid: this.pet.uuid, name: this.getFormattedName(), lore: [], attributes: { pet: 'true', rarity: this.rarity, }, }; } getProgress(stats, player) { const queryStats = stats && stats.length > 0 ? stats : [Stat.FarmingFortune]; const currentStats = this.getStatTotals(queryStats, player); const maxPet = this.withChanges({ exp: this.getXpForLevel(this.info.maxLevel ?? 100) }); const maxStats = maxPet.getStatTotals(queryStats, player); const perStat = {}; for (const stat of queryStats) { const current = currentStats[stat] ?? 0; const max = Math.max(maxStats[stat] ?? 0, current); if (current === 0 && max === 0) continue; perStat[stat] = { current, max, ratio: Math.min(max === 0 ? 0 : current / max, 1), }; } const current = currentStats[Stat.FarmingFortune] ?? this.getFortune(Stat.FarmingFortune, player); const max = Math.max(maxStats[Stat.FarmingFortune] ?? 0, current); const upgrades = this.getUpgrades({ stats: queryStats }, player); const breakdownProgress = this.getBreakdownProgress(maxPet, queryStats, player); return [ { name: `${this.info.name} Pet`, current, max, ratio: Math.min(max === 0 ? 0 : current / max, 1), stats: Object.keys(perStat).length > 0 ? perStat : undefined, item: this.getProgressItem(), wiki: this.info.wiki, upgrades: upgrades.length > 0 ? upgrades : undefined, progress: breakdownProgress.length > 0 ? breakdownProgress : undefined, }, ]; } getUpgrades(options, player) { const stats = getQueryStats(options); const upgrades = []; const maxLevel = this.info.maxLevel ?? 100; if (this.level < maxLevel) { const nextLevel = this.level + 1; const nextPet = this.withChanges({ exp: this.getXpForLevel(nextLevel) }); const deltaStats = this.getDeltaStats(nextPet, player); const increase = deltaStats[Stat.FarmingFortune] ?? 0; const upgrade = { title: `${this.info.name} Level ${nextLevel}`, increase, stats: deltaStats, action: UpgradeAction.LevelUp, category: UpgradeCategory.Pet, wiki: this.info.wiki, onto: { name: this.getFormattedName(), skyblockId: this.type, }, meta: { type: 'pet_level', itemUuid: this.pet.uuid ?? undefined, value: nextLevel, }, conflictKey: `pet-level:${this.pet.uuid ?? this.type}`, }; if (improvesRequestedStat(upgrade, stats)) { upgrades.push(upgrade); } } const currentItemId = this.item ? getPetItemId(this.item) : undefined; for (const [itemId, item] of Object.entries(FARMING_PET_ITEMS)) { if (itemId === currentItemId) continue; const nextPet = this.withChanges({ heldItem: itemId }); const deltaStats = this.getDeltaStats(nextPet, player); const increase = deltaStats[Stat.FarmingFortune] ?? 0; const upgrade = { title: item.name, increase, stats: deltaStats, action: UpgradeAction.Apply, category: UpgradeCategory.Pet, purchase: itemId, wiki: item.wiki, onto: { name: this.getFormattedName(), skyblockId: this.type, }, cost: { items: { [itemId]: 1, }, }, meta: { type: 'pet_item', id: itemId, itemUuid: this.pet.uuid ?? undefined, }, conflictKey: `pet-item:${this.pet.uuid ?? this.type}`, }; if (improvesRequestedStat(upgrade, stats)) { upgrades.push(upgrade); } } upgrades.sort((a, b) => (b.increase ?? 0) - (a.increase ?? 0)); return upgrades; } getChimeraAffectedStats(multiplier) { const result = {}; // Item stats for (const [key, stat] of Object.entries(this.item?.stats ?? {})) { const value = getStatValue(stat, this.options); if (result[key]) { result[key] += value * multiplier; } else { result[key] = value * multiplier; } } // Base stats for (const [key, stat] of Object.entries(this.info.stats ?? {})) { if (stat.type !== FarmingPetStatType.Base) continue; const value = getStatValue(stat, this); if (result[key]) { result[key] += value * multiplier; } else { result[key] = value * multiplier; } } // Per level stats for (const [key, stat] of Object.entries(this.info.perLevelStats ?? {})) { if (stat.type !== FarmingPetStatType.Base) continue; const value = getStatValue(stat, this); if (result[key]) { result[key] += value * this.level * multiplier; } else { result[key] = value * this.level * multiplier; } } return result; } static isValid(pet) { return pet.type && pet.type in FARMING_PETS; } static fromArray(items, options) { return items .filter((item) => FarmingPet.isValid(item)) .map((item) => new FarmingPet(item, options)) .sort((a, b) => b.fortune - a.fortune); } } //# sourceMappingURL=farmingpet.js.map