Skip to content

Builders

Josh Dionne edited this page Aug 25, 2026 · 2 revisions

Generated from the thetowersdk repository. Edits made here are overwritten on the next push — change the source file instead.

Builders

Ready-made calculators. Each one pairs the maths in thetowersdk/mechanics with the things a tool needs around it and never gets for free: a complete set of defaults, a description of every input, normalisation of whatever half-filled state a form is in, and a result that says what it could not work out.

import { labResearchCalculator } from 'thetowersdk/builders'

const result = labResearchCalculator.compute({ labName: 'Damage', targetLevel: 10 })
console.log(result.totalCoinCost, result.totalHours)
for (const note of result.notes) console.warn(note)

compute takes a Partial on purpose — a form hands you half-filled state constantly, and a calculator that throws or returns NaN on that pushes the problem back into the UI.

Render a form without knowing which calculator it is:

for (const field of labResearchCalculator.fields) {
  // field.kind is 'number' | 'select' | 'boolean' | 'number-list'
}
Builder Answers
labResearchCalculator Coins and time to take a lab from one level to another
workshopUpgradeCalculator Coins to move a workshop stat between two levels
moduleCostCalculator Shards and coins to level a module, capped by its rarity
ultimateWeaponCalculator Power Stones for an ultimate weapon stat
uptimeCalculator What share of the time an ability is actually running
guardianCalculator Bits for a guardian stat
botUpgradeCalculator Medals for a bot stat
enemyWaveCalculator Health and damage per enemy kind at a tier and wave
damageReductionCalculator What reaches the tower after every mitigation layer
assistModuleStonesCalculator Stones for an assist efficiency slot
coinsPerKillCalculator What one enemy pays, all six bonus sources applied
thornsCalculator Damage returned to an enemy on contact
dissonanceCalculator The multiplier your tier personal bests apply to a stat
enemyDropsCalculator Module drop chances, reroll shards, shatter shards
innerLandMinesCalculator Mine damage, count, cooldown, and what charge time is worth

CALCULATOR_BUILDERS is all of them, and findCalculatorBuilder(id) looks one up — enough to build a calculator picker that needs no per-calculator code at all.

They wrap thetowersdk/mechanics rather than reimplementing it, so a correction to a formula reaches every tool without anyone re-deriving it.

Caps, clamps and level indexing

Every builder handles these the same way, so a number means the same thing whichever one produced it.

Rule How
A level is capped by its own catalog clampLevel(value, cap, fallback), where cap comes from the curve that stat actually uses
An open-ended quantity is bounded so the result stays finite clampMagnitude(value, fallback), bounded by MAX_INPUT_MAGNITUDE
A level buys against the previous row costIndexForLevel(level) — buying level L reads entry L−1
Anything only a formula bounds gets a named constant e.g. MAX_MODEL_WAVE, where the wave curve stops producing a real number
Clamping is reported the result's notes say what was clamped and to what

Caps come from the data rather than a shared constant because the curves genuinely differ: workshop Attack Speed prices 75 levels, Enemy Level Skip 60, Damage 400. Level indexing follows tower-oracle workshop.upgradeTable: a cost row buys the next level, so entry L is what is charged at level L and buying level L costs entry L−1.

Lookups by a name you did not choose

Ids, slugs and level indices arrive from saves, from the community sheet, from URLs and from tool arguments. Lookups keyed by them use own-key access (ownLookup) for records and a bounds-and-integer check (atIndex) for arrays, so an unknown key returns undefined rather than whatever sits on Object.prototype.

The reason it is a rule rather than a caution: record['constructor'] is the Object function and record['toString'] is a function, and neither ?? nor || nor a truthiness check will reject one. The fallback simply never fires, and a function travels on from a signature that promised a string.

Cost ladders are bounded the same way, by the table rather than by the caller: a level past the end of a curve is excluded rather than priced at zero.


Clone this wiki locally