Live game economies look chaotic from the outside. Underneath they are usually a short list of multipliers applied in a fixed order. The engineering problem is not how to win the game. It is how you encode the sell formula so a client-side tool stays honest after patches, refuses illegal stacks, and does not silently invent value.
This walkthrough uses a compact JavaScript model for a Roblox farming sequel: a power-law weight term, a single mutation seat, a harvest-type keep rule, friend and freshness multipliers, and a hard cap on a luck statistic. The same pattern applies to any domain calculator that must turn a product of rules into a number a user can trust.
Start with an ordered formula
If the UI drives the math, users click every bonus and you overcount. Invert that. Write the formula first, then bind controls to named terms.
value = base
* pow(weight / baseWeight, EXPONENT)
* mutationEffective
* friendFactor
* freshness
* quantity
* promo
Two choices matter more than branding:
- Order is part of the spec. Freshness after mutation is not the same as freshness inside the exponent.
-
Illegal combinations must be unrepresentable. If the live game allows only one mutation, the model holds a single
mutationMult, not an array you later mostly ignore.
Community testing for this sequel settled on an exponent near 2.65. That is steeper than a simple square. A calculator that uses ** 2 because "weight usually squares" will systematically underprice heavy fruit.
Name the invariants next to the code
const SIZE_EXPONENT = 2.65;
const FRIEND_CAP = 7;
const SINGLE_HARVEST_KEEP = 0.15;
const LUCK_CAP = 100;
function sizeMult(weight, baseWeight) {
if (!(baseWeight > 0) || !(weight >= 0)) return 0;
return Math.pow(weight / baseWeight, SIZE_EXPONENT);
}
function mutationEffective(mult, isSingleHarvest) {
if (!isSingleHarvest) return mult;
return 1 + (mult - 1) * SINGLE_HARVEST_KEEP;
}
function friendFactor(friends) {
const n = Math.min(FRIEND_CAP, Math.max(0, friends | 0));
return (10 + n) / 10;
}
The single-harvest keep rule is easy to get wrong. Players intuitively apply 0.15 * 60 and get 9. The rule is keep 15% of the bonus above 1x:
1 + (60 - 1) * 0.15 = 9.85
If your tests skip that case, the UI looks fine and the numbers are wrong on the crops where mutation chasing is weakest.
One seat means one selected control
A common porting bug is copying a stacking UI from a previous title. New weather should overwrite the sticker, not push onto a list.
Luck works the same way. Multiple sprinklers do not add past a cap of 100. Only the strongest in-range unit counts:
luck = min(100, max(appliedLuckValues))
Extra devices may still help coverage or growth speed. They must not inflate a sell term that the game does not apply.
Keep promo flags off by default
A checked box that adds ×2.2 will be left on after the promo ends. Defaults should encode the conservative world. Show a breakdown next to the total (size, mutEff, friends, fresh, promo). Users forgive a rounded integer. They do not forgive a silent multiplier they did not earn.
Keep full floats through the product. Round once for display. Never round intermediate sizeMult to two decimals if later terms are large.
Ship a pure function, then wrap the DOM
Separate estimate(input) from readForm() / render(result). That split lets you unit-test the engine in Node without JSDOM, and reuse the same function for a reverse solver (target value -> required weight).
The open-source engine and tests live in gag2-sell-engine. A full browser implementation of this shape is the Grow a Garden 2 calculator. Treat it as a worked demo of the model, not as a substitute for the live game tooltip after a patch.
What I would change next
- Version the formula (
gag2-sell-v1) and show that version in the UI footer. - Load mutation multipliers from a dated JSON file so a patch does not require an HTML rewrite.
- Fail closed: unknown crop id returns
null, not0, so the UI can say "missing data" instead of "worthless."
Domain calculators fail when they optimize for looking complete. They succeed when every multiplier has an owner, a default, and a test.
Top comments (0)