DEV Community

姜炜奇
姜炜奇

Posted on

Zero, missing, or triggered: a small formatter for game-reference stats

Disclosure: I maintain AzerothSurvivors.app, an independent fan reference for Aethus's game. This article and its illustrative code were prepared with AI assistance. The code below is a standalone example, not extracted game code or a claim about the site's implementation.

A stat table can be numerically tidy and still be misleading. A cooldown of zero, an unavailable cooldown, and an effect activated by an event should not all render as 0 s.

The problem is especially easy to introduce while converting loosely structured data for a static page. A default such as value || 0 silently treats missing information as a known zero. Converting everything to a number before checking it can also erase the distinction. MDN documents that Number.isFinite() checks the supplied value without first coercing a string or null to a number.

For a small reference, I prefer making the states explicit at the boundary and formatting them only after validation.

Three states, not three guesses

In this example, a numeric record must contain a finite JavaScript number and an explicit unit. An unknown record renders as "Not recorded". A triggered record requires a short description of its event instead of pretending that it has a periodic interval.

These are presentation states. They do not determine whether a real game effect has an internal cooldown, whether the source is correct, or how a modifier is applied. A source can contain both an event trigger and a numeric cooldown; model those as separate fields rather than forcing one to replace the other.

const units = new Set(['s', '%', 'damage', 'targets']);

export function formatStat(record) {
  if (!record || typeof record !== 'object' || Array.isArray(record)) {
    throw new TypeError('Expected a stat record');
  }
  switch (record.kind) {
    case 'unknown':
      return 'Not recorded';
    case 'number': {
      if (!Number.isFinite(record.value)) {
        throw new TypeError('Expected a finite number, without coercion');
      }
      if (!units.has(record.unit)) {
        throw new TypeError('Unknown unit');
      }
      return `${record.value} ${record.unit}`;
    }
    case 'triggered': {
      if (typeof record.event !== 'string' || !record.event.trim()) {
        throw new TypeError('Expected a nonempty event description');
      }
      return `Triggered: ${record.event.trim()}`;
    }
    default:
      throw new TypeError('Unknown stat kind');
  }
}
Enter fullscreen mode Exit fullscreen mode

The unit names are illustrative. Adapt the allowed set to your own schema. The function deliberately performs no percentage conversion, no rounding, and no automatic substitution of missing fields. A value of 5 with unit % means 5 % here, not an inferred fraction to multiply by 100.

Test the distinction at the boundary

Save the function as format-stat.mjs and the following as format-stat.test.mjs, then run node format-stat.test.mjs. These twelve checks cover the formatter, not combat balance or the accuracy of a game's data.

import assert from 'node:assert/strict';
import { formatStat } from './format-stat.mjs';

assert.equal(formatStat({ kind: 'number', value: 0, unit: 's' }), '0 s');
assert.equal(formatStat({ kind: 'unknown' }), 'Not recorded');
assert.equal(formatStat({ kind: 'triggered', event: ' on hit ' }), 'Triggered: on hit');
assert.equal(formatStat({ kind: 'number', value: 1e-7, unit: 's' }), '1e-7 s');
assert.equal(formatStat({ kind: 'number', value: 5, unit: '%' }), '5 %');
assert.throws(() => formatStat({ value: 0, unit: 's' }), TypeError);
assert.throws(() => formatStat({ kind: 'number', value: '0', unit: 's' }), TypeError);
assert.throws(() => formatStat({ kind: 'number', value: NaN, unit: 's' }), TypeError);
assert.throws(() => formatStat({ kind: 'number', value: Infinity, unit: 's' }), TypeError);
assert.throws(() => formatStat({ kind: 'triggered', event: '  ' }), TypeError);
assert.throws(() => formatStat({ kind: 'number', value: 1, unit: 'mystery' }), TypeError);
assert.throws(() => formatStat(null), TypeError);
console.log('12 formatter checks passed');
Enter fullscreen mode Exit fullscreen mode

Notice the small positive number. Rounding it to an integer would show zero, which communicates a different value. A production interface can choose a more readable numeric format, but that choice should be tested separately from the meaning of an unavailable value.

Keep provenance beside the presentation

A formatter cannot fix an undocumented source. Keep a version label and the source scope with the data. In the Azeroth Survivors abilities reference, the baseline is explicitly labelled; the equipment reference serves a different lookup purpose. Links between the two do not make a displayed baseline equivalent to measured damage during a run.

For a browser view, insert the returned label as text rather than interpreting event descriptions as HTML. If a record fails validation, report the affected record to the editor instead of quietly turning it into zero. That gives a missing field a repair path and keeps a clean-looking table from making an unsupported claim.

The fan reference is not affiliated with Aethus or Blizzard Entertainment and does not host or redistribute game files.

Top comments (0)