跳到正文
DEV Community· 姜炜奇·· 2 小時前AI 評分46

零值、未記錄或事件觸發:遊戲參考數據的小型格式化函數

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

AI 導讀

文章示範一個 JavaScript 格式化函數,將數值為零、資料未知和由事件觸發的遊戲屬性分開呈現,避免把缺失資料誤顯示為 0。函數要求數值為有限數字並附有明確單位;未知資料顯示為「Not recorded」,事件觸發資料則須提供事件描述。12 項測試涵蓋格式化結果及無效資料的驗證。

正文

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');
  }
}

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');

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.

來源:DEV Community · dev.to