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.
What the game stats are
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.
Live or pinned
Pick once, deliberately, and write the choice down where the rest of your build config lives.
| What you are building | What to read | Why |
|---|---|---|
| A shipped game build | GET /v1/game-stats/versions/{version}/firearms | Frozen numbers. A rebalance cannot change a weapon between a playtest and a release. |
| A website, wiki or comparison tool | GET /v1/firearms/{id}/game-stats | Live numbers, which is what a reader expects from a page about the current catalog. |
| Tuning your own weapon table | GET /v1/game/tier-list?stat= | S to D bands for one stat, which is the shape a designer argues with. |
| Finding your own outliers | GET /v1/game/balance-report | Distribution warnings across the stat set: what is over- or under-tuned relative to everything else. |
Pinning a snapshot
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.
// 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.
Reading live, on purpose
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.
// 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.The analysis endpoints
Four calls that exist so you do not have to compute them over a mirror; the game reference has the parameters.
/v1/game/tier-listbands firearms S to D for one stat./v1/game/matchupscompares two directly, and/role-rosterfills a role such as marksman or breacher from the catalog./v1/game/stat-distributionis the histogram behind a stat, which is how you decide whether your own curve is unusual./v1/game/balance-reportis Studio; the other three are Builder.