farming-weight
Version:
Tools for calculating farming weight and fortune in Hypixel Skyblock
239 lines • 8.42 kB
JavaScript
import { isGlobalOverbloomScope, matchesScopeForDrop, matchesScopeForStat } from './matcher.js';
import { DEFAULT_PHASE_FOR_OP, } from './types.js';
/** Resolve an effect's phase, defaulting from its op when omitted. */
export function effectPhase(effect) {
return effect.phase ?? DEFAULT_PHASE_FOR_OP[effect.op];
}
/** Resolve the numeric `value` of an effect at a given context. */
function resolveValue(effect, ctx) {
const { value } = effect;
if (typeof value === 'function')
return value(ctx);
return typeof value === 'number' ? value : 0;
}
/**
* Throw if a multiplicative effect carries an invalid value. Catches the
* Cropeetle/Deep-Fried delta-vs-factor mismatch class of bugs at the source.
*/
export function assertValidEffect(effect) {
if (effect.op === 'mul-rare' || effect.op === 'mul-drop') {
const v = effect.value;
const numeric = typeof v === 'number' ? v : null;
if (numeric !== null && numeric < 0) {
throw new Error(`Invalid ${effect.op} effect from "${effect.source}": value must be >= 0 (got ${numeric}). ` +
'Multiplicative ops carry a factor (1.2 = x1.2), not a delta.');
}
}
if (effect.op === 'add-stat' && !effect.stat) {
throw new Error(`Invalid add-stat effect from "${effect.source}": missing 'stat' field.`);
}
if (effect.op === 'add-drop' && !effect.drop) {
throw new Error(`Invalid add-drop effect from "${effect.source}": missing 'drop' field.`);
}
}
/**
* Sum `add-stat` effects matching a stat query and a `StatContext`.
*
* Drop-only scoped effects are rejected by the matcher.
*/
export function resolveStatTotal(effects, stat, ctx) {
let total = 0;
for (const effect of effects) {
if (effect.op !== 'add-stat')
continue;
if (effect.stat !== stat)
continue;
if (!matchesScopeForStat(effect.scope, ctx))
continue;
total += resolveValue(effect, ctx);
}
return total;
}
/**
* Per-stat breakdown of `add-stat` contributions matching a `StatContext`.
* Returns `{ source: amount }` aggregated by source name.
*/
export function resolveStatBreakdown(effects, stat, ctx) {
const out = {};
for (const effect of effects) {
if (effect.op !== 'add-stat')
continue;
if (effect.stat !== stat)
continue;
if (!matchesScopeForStat(effect.scope, ctx))
continue;
const v = resolveValue(effect, ctx);
if (!v)
continue;
out[effect.source] = (out[effect.source] ?? 0) + v;
}
return out;
}
/**
* Compute the virtual scalar for `Stat.Overbloom`.
*
* Sums `value` from `add-rare-pct` effects whose `relatedStats` includes
* `Stat.Overbloom` and whose scope is global Overbloom-shaped. Scoped Overbloom-
* flavored effects are excluded by design - see the architecture plan.
*/
export function resolveOverbloomScalar(effects, ctx, overbloomStat) {
let total = 0;
for (const effect of effects) {
if (effect.op !== 'add-rare-pct')
continue;
if (!effect.relatedStats?.includes(overbloomStat))
continue;
if (!isGlobalOverbloomScope(effect.scope))
continue;
// Env-level requirements still apply to the scalar (e.g. Harvest Feast-only flat Overbloom would only
// contribute when the feast is active). `requiresInSeason` requires a crop in the StatContext.
if (effect.scope?.requiresHarvestFeast && !ctx.env.harvestFeast)
continue;
if (effect.scope?.requiresInSeason && (!ctx.crop || !ctx.env.inSeason))
continue;
total += resolveValue(effect, ctx);
}
return total;
}
/**
* Per-source breakdown of the global Overbloom scalar.
*/
export function resolveOverbloomBreakdown(effects, ctx, overbloomStat) {
const out = {};
for (const effect of effects) {
if (effect.op !== 'add-rare-pct')
continue;
if (!effect.relatedStats?.includes(overbloomStat))
continue;
if (!isGlobalOverbloomScope(effect.scope))
continue;
if (effect.scope?.requiresHarvestFeast && !ctx.env.harvestFeast)
continue;
if (effect.scope?.requiresInSeason && (!ctx.crop || !ctx.env.inSeason))
continue;
const v = resolveValue(effect, ctx);
if (!v)
continue;
out[effect.source] = (out[effect.source] ?? 0) + v;
}
return out;
}
/**
* Collect drops emitted by `add-drop` effects whose env-level scope matches.
*
* Real per-drop scope filtering (tags, items, etc.) happens later when the
* calculator builds a `DropContext` for each candidate.
*/
export function produceAddedDrops(effects, env) {
const out = [];
for (const effect of effects) {
if (effect.op !== 'add-drop')
continue;
if (!effect.drop)
continue;
const scope = effect.scope;
if (scope) {
if (scope.crops) {
if (!env.crop)
continue;
if (!scope.crops.includes(env.crop))
continue;
}
if (scope.requiresHarvestFeast && !env.harvestFeast)
continue;
if (scope.requiresInSeason && !env.inSeason)
continue;
}
out.push({ source: effect.source, payload: effect.drop });
}
return out;
}
/**
* Resolve `add-rare-pct`, `mul-rare`, and `mul-drop` effects for a single drop.
*
* The calculator is expected to combine the parts as:
* `final = base * (1 + addRarePct/100) * mulRare * mulDrop`
*
* `applied` records every effect that touched this drop, with phase and
* resolved amount, for breakdown UIs.
*/
export function resolveDropEffects(effects, ctx) {
let addRarePct = 0;
let mulRare = 1;
let mulDrop = 1;
const applied = [];
// Phase: add-rare
for (const effect of effects) {
if (effect.op !== 'add-rare-pct')
continue;
if (!matchesScopeForDrop(effect.scope, ctx))
continue;
const v = resolveValue(effect, ctx);
if (!v)
continue;
addRarePct += v;
applied.push({
source: effect.source,
op: effect.op,
phase: 'add-rare',
amount: v,
relatedStats: effect.relatedStats,
scope: effect.scope,
description: effect.meta?.description,
valueDisplay: effect.meta?.valueDisplay,
valueStat: effect.meta?.valueStat,
});
}
// Phase: mul-rare
for (const effect of effects) {
if (effect.op !== 'mul-rare')
continue;
if (!matchesScopeForDrop(effect.scope, ctx))
continue;
const v = resolveValue(effect, ctx);
if (v < 0) {
throw new Error(`Invalid mul-rare value ${v} from "${effect.source}" (must be >= 0; factor semantics).`);
}
if (v === 1)
continue;
mulRare *= v;
applied.push({
source: effect.source,
op: effect.op,
phase: 'mul-rare',
amount: v,
relatedStats: effect.relatedStats,
scope: effect.scope,
description: effect.meta?.description,
valueDisplay: effect.meta?.valueDisplay,
valueStat: effect.meta?.valueStat,
});
}
// Phase: mul-drop
for (const effect of effects) {
if (effect.op !== 'mul-drop')
continue;
if (!matchesScopeForDrop(effect.scope, ctx))
continue;
const v = resolveValue(effect, ctx);
if (v < 0) {
throw new Error(`Invalid mul-drop value ${v} from "${effect.source}" (must be >= 0; factor semantics).`);
}
if (v === 1)
continue;
mulDrop *= v;
applied.push({
source: effect.source,
op: effect.op,
phase: 'mul-drop',
amount: v,
relatedStats: effect.relatedStats,
scope: effect.scope,
description: effect.meta?.description,
valueDisplay: effect.meta?.valueDisplay,
valueStat: effect.meta?.valueStat,
});
}
return { addRarePct, mulRare, mulDrop, applied };
}
//# sourceMappingURL=resolver.js.map