GunSpec
Practice

Game stats without balance drift

The game fields are an editorial balance layer, normalised 0 to 100. They move when the catalog is rebalanced, which is why a build pins a snapshot.

Eight comparable values derived from the physical record: damage, accuracy, range, fire rate, mobility, recoil control, reload speed and concealment. The field reference defines each one.

  • They are comparable across the whole catalog, which is the point: a 70 on one rifle means what a 70 means on another.
  • They are editorial, not measured. The measured figures behind them (weight, barrel, muzzle energy, capacity) are on the record itself, and that is what a simulation should read.
  • They are rebalanced with the catalog. Nothing about a rebalance is a breaking API change, so nothing announces it to a build reading the live endpoint.

Pick once, deliberately, and write the choice down where the rest of your build config lives.

What you are buildingWhat to readWhy
A shipped game buildGET /v1/game-stats/versions/{version}/firearmsFrozen numbers. A rebalance cannot change a weapon between a playtest and a release.
A website, wiki or comparison toolGET /v1/firearms/{id}/game-statsLive numbers, which is what a reader expects from a page about the current catalog.
Tuning your own weapon tableGET /v1/game/tier-list?stat=S to D bands for one stat, which is the shape a designer argues with.
Finding your own outliersGET /v1/game/balance-reportDistribution warnings across the stat set: what is over- or under-tuned relative to everything else.

Import the snapshot into your own data files at the start of a cycle, and move it when you choose to. Treat it exactly as you treat an asset bundle version.

import-snapshot.ts
javascript
// Pick a version once, at the start of a development cycle,// and put it in your build config beside the asset version.const { data: versions } = await gunspec.gameStats.listVersions()// versions[0] is the newest. Choose deliberately; do not take [0]// at runtime, or you have re-implemented the live endpoint.const PINNED = '2026-09-01' // Everything frozen into that snapshot, paged. Import it once into// your own data files; do not call this at match start.const { data } = await gunspec.gameStats.listFirearms(PINNED, { per_page: 100 })
  • Do not resolve the newest version at runtime: that is the live endpoint again, with extra steps.
  • Import once, ship the data with the build. Match start is not the time to be making HTTP requests, and it is quota spent per player rather than per release.
  • Record the version in your repository. When a balance complaint arrives, the first question is which snapshot the build was on.
  • Diff the new snapshot against your pinned one before moving. What changed is a design decision, not an upgrade.

A site about the catalog should track the catalog. Cache it like any other record, and say on the page that the numbers are a balance layer rather than measurements.

live-stats.ts
javascript
// The live numbers, which track the catalog and change when it// is rebalanced. Right for a website, a wiki or a comparison tool -// wrong for a build that has to behave the same next week.const { data: stats } = await gunspec.firearms.getGameStats('glock-g17') // Each stat is 0-100 and comparable across the catalog: they are a// balance layer, not a measurement. The physical figures they were// derived from are on the record itself.

Four calls that exist so you do not have to compute them over a mirror; the game reference has the parameters.

  • /v1/game/tier-list bands firearms S to D for one stat.
  • /v1/game/matchups compares two directly, and /role-roster fills a role such as marksman or breacher from the catalog.
  • /v1/game/stat-distribution is the histogram behind a stat, which is how you decide whether your own curve is unusual.
  • /v1/game/balance-report is Studio; the other three are Builder.