Warcraft catalog: turn game observations into useful information
A recorder tells your app which units exist and what is happening. The catalog
adds what those Warcraft objects mean: their costs, base values, ability levels,
production relationships and artwork. Join the two by typeId to build army
panels, mana indicators, research views and richer tooltips without maintaining
your own rawcode tables and icon collection.
The catalog is available through @w3booster/sdk/game-data. It is a separate,
optional entry point, with data and images hosted outside your app bundle. Each
match names its exact catalog revision in match.gameDataId, so a replay can
use the data for its recorded Warcraft build rather than today's balance values.
What you can build
| App feature | Catalog data | Live observation |
|---|---|---|
| Army composition and cost ordering | Unit gold/lumber cost, supply and icon | Observed units and their typeId |
| Ability mana indicator | Mana cost for the learned ability level | Current hero mana and observed recovery rate |
| Research panel | Upgrade category, level names, costs, research duration and artwork | Active upgrade type, level and progress |
| Building tier transition | Destination building name and icon | building.upgrade destination and progress |
| Inventory tooltip | Item name, cost, icon and linked ability IDs | Slot contents and directly observed slot cooldowns |
| Unit reference or tech browser | Base stats, abilities, train/build relationships | Optional selection or observed ownership |
Catalog access does not expand an app's permission to observe live units or players. Keep the normal SDK access rules when combining public definitions with match state.
Four typed collections and generated artwork
| Collection | Available definitions |
|---|---|
data.units |
Gold/lumber cost; supply used/provided; base health, mana, armor, movement and attributes; build time; abilities, upgrades, trained and built units. |
data.abilities |
Maximum level, required hero level and per-level mana cost, cooldown, cast time and range. |
data.items |
Name, gold/lumber cost and linked abilities. |
data.upgrades |
Category, maximum level and ordered levels with name, cost and research time. |
Each collection offers get(typeId), has(typeId) and values(). Values can be
absent; an unknown cost or stat is not zero. Use data.assets.unitIcon,
abilityIcon, itemIcon or upgradeIcon for hosted, content-hashed artwork.
Choose classic or reforged to match the game; ability and upgrade artwork can
also use a one-based level and the icon, research or inactive role. Objects
without configured art can legitimately have no image.
Start with the match's revision
The following checked example builds an army row and an ability presentation.
It uses actual typeId values, retains observed health and mana, and only
estimates mana recovery time when a positive rate is available.
import type { MatchState, Unit, Hero, HeroAbility } from '@w3booster/sdk';
import { loadGameData, abilityCooldown } from '@w3booster/sdk/game-data';
export async function catalogDetails(
state: MatchState, unit: Unit, hero: Hero, ability: HeroAbility,
signal?: AbortSignal
) {
const revision = state.match.gameDataId;
if (!revision) return undefined;
const data = await loadGameData(revision, { signal });
const graphics = state.match.isReforged ? 'reforged' : 'classic';
const definition = data.units.get(unit.typeId);
const level = ability.level > 0
? data.abilities.get(ability.typeId)?.levels[ability.level - 1]
: undefined;
const manaCost = level?.manaCost;
const mana = hero.mana;
const missingMana = mana && manaCost !== undefined
? Math.max(0, manaCost - mana.current) : undefined;
const rate = mana?.regenerationPerSecond;
const secondsUntilMana = missingMana === 0 ? 0
: missingMana !== undefined && rate !== undefined && rate > 0
? missingMana / rate : undefined;
return {
unit: {
name: definition?.name, cost: definition?.cost,
supply: definition?.supply,
icon: data.assets.unitIcon(unit.typeId, { graphics }),
hitpoints: unit.hitpoints
},
ability: {
manaCost, missingMana, secondsUntilMana,
icon: ability.level > 0 ? data.assets.abilityIcon(ability.typeId,
{ graphics, level: ability.level }) : undefined,
cooldown: abilityCooldown(ability, state.match.gameTime, data)
}
};
}
Call the loader when the advertised revision changes, then reuse the returned
catalog across snapshots. Successful loads are cached by revision, base URL and
fetch implementation. Failed or aborted loads can be retried. For asynchronous
UI work, discard results if the match or revision changed while loading; an
AbortSignal also lets you cancel work when the view closes.
If gameDataId is absent or loading fails, continue showing the live state and
omit unavailable catalog details. Never silently substitute the latest catalog,
a nearby build, a similarly named object or a guessed icon.
Practical details that matter
Army values and ordering
Copy SDK arrays before sorting them. Choose a clear cost order, for example gold
ascending, then lumber ascending, then typeId for ties, with unknown values
last. Gold and lumber have no universal exchange rate: multiplying a unit's
catalog costs by observed counts gives separate gold and lumber values, not a
single authoritative army-strength score. Also decide explicitly whether your
view includes workers, buildings, summons and illusions.
Ability levels, mana and cooldowns
ability.level is one-based; level zero means unlearned. Index catalog levels
with level - 1 only for learned abilities. Rawcodes are case-sensitive and keep
their actual variants: do not normalize an ability into a similar-looking one.
A mana bar can show currentMana / manaCost, clamped to 0–1, and disappear once
the cost is met. Mana sufficiency alone does not prove an ability is castable:
cooldowns, targets, silence and other game rules may still prevent casting.
regenerationPerSecond is an observed net change per game second, so an
estimate of missing mana divided by a positive rate can change after the next
sample. Zero, negative or unavailable rates do not supply a finite estimate.
abilityCooldown(ability, gameTime, data) derives an estimate from the observed
activation and catalog duration. abilityCooldownsForState provides the
whole-state equivalent and checks the revision. These are different from the
engine timers in hero.inventoryCooldowns, which are aligned to inventory slots.
Use the match game clock; do not advance an observed snapshot with wall-clock
time while a replay is paused.
Inventory capacity and remaining charges
Hero.inventoryCharges supplies live remaining counts, aligned with
hero.inventory. The catalog's optional ItemType.initialCharges supplies the
normal starting capacity. Keep both arrays in slot order, including empty slots
and duplicate item types. A known 0 stays zero; null means an empty or
unobserved slot, and an absent array means counts are unavailable.
For example, these hero fields represent two salves, a single-use potion and three empty slots:
{
"inventory": ["hslv", "hslv", "phea", "", "", ""],
"inventoryCharges": [3, 1, 1, null, null, null]
}
The salve's catalog definition has initialCharges: 3. A compact indicator can
show ● ○ ○ for the second salve, while hiding indicators for single-use items.
Zero remaining renders three empty dots. Hide unavailable counts; never replace
them with the starting default. Use a number if the live count exceeds capacity
or if the capacity is too large for your icon.
import type { Hero } from '@w3booster/sdk';
import type { GameData } from '@w3booster/sdk/game-data';
// Load data from the current match.gameDataId; reuse it across snapshots.
export function inventoryChargeIndicator(hero: Hero, slot: number, data: GameData) {
const typeId = hero.inventory?.[slot];
const initial = typeId ? data.items.get(typeId)?.initialCharges : undefined;
const remaining = hero.inventoryCharges?.[slot];
if (initial === undefined || initial <= 1 || remaining == null) return undefined;
return {
remaining,
// Keep large/custom counts readable and bound the number of dots.
dots: remaining <= initial && initial <= 10
? Array.from({ length: initial }, (_, index) => index < remaining)
: null // Render a numeric badge instead.
};
}
This is standard capacity, not a recorded starting count for that item instance
or proof of how many uses occurred. Custom maps and modified items may differ.
Starting charges are distinct from shop stock and item cooldowns. Subscribe to
state or hero.changed to refresh counts even when inventory rawcodes stay the
same. Existing heroes:read and player-ownership rules still apply. Older
recorders or catalogs can omit either optional field.
Upgrade levels and building transitions
Resolve research through data.upgrades.get(upgrade.typeId) and select
levels[upgrade.level - 1]. Use the same one-based level for its artwork; do not
parse a level suffix from the rawcode. Categories such as armor, melee,
ranged and caster come from the source data.
A building upgrading into another building is a separate activity:
building.upgrade.typeId resolves through data.units, and its observed
progress remains authoritative. Do not infer that progress from the catalog's
base build time or merge it into the training/research queue.
Go deeper with the optional gameplay projection
await data.unitGameplay(typeId) loads a broader retained unit-data projection
on demand. It includes source fields for attacks, targeting, damage dice,
movement, regeneration, attributes, production, prerequisites and related
configuration. Values are source strings grouped by source section, including
empty values and sentinels; this is not another set of normalized live stats.
Use the typed collections for common UI. Reach for the broad projection when building an inspection tool or an explicitly defined calculation that needs source fields beyond the small typed view. The optional fetch and decoding keep that extra data off the ordinary application path.
What the catalog does—and does not—represent
The pipeline extracts the build's current-melee object configuration and exact artwork references together. The SDK verifies checksums and build identity and exposes immutable data. English display names are labels; rawcodes are keys. Classic and Reforged references are selected during generation rather than constructed by guessing filenames in the app.
Base configuration is not effective live combat state. Hero attributes, learned upgrades, buffs and game rules can change health, damage, armor or mana costs. Prefer observed values whenever they exist. Do not present a base-health field as a hero's current maximum or precompute an authoritative live DPS from catalog damage alone.
The dataset describes current melee rules for that build; it does not resolve custom-map overrides. Item definitions currently expose names, costs and linked abilities, not normalized numeric item damage or armor effects. Never parse an item's display name or rawcode suffix as its numeric balance value.