Repository navigation
How the Build Generator Works
src/generator/ is a deterministic, pure function of (hero assets, item catalog, aggregate analytics). It never reads per-player
data; npm run verify greps the module for any panel account id and checks two runs give identical output.
One build is produced per hero unless the analytics show two established ways to play it, in which case one
build is produced per style, each scored from that style's own population of games (see Build Styles).
The scoring archetype (stats.ts → ARCHETYPES) has neutral stat multipliers (1.0) and no slot bias, so the
builds differ only because their populations differ.
All weights and limits live in one object, PARAMS in stats.ts. The current values were chosen by
scripts/tune.ts to maximise the mean panel agreement across all 38 heroes (2026-09-06); see
Held-out Validation and Judgment Calls.
For each candidate item i (shopable, cost > 0, relative usage ≥ 12 %):
score(i) = 2.0 · sqrt(pop) pop = matches_i / matches of the hero's most-bought item
+ 0.25 · winLift winLift = (shrunkWR_i − heroMeanWR) × 10 × pop
+ 0 · eff / max(eff) eff = Σ_stat |value| · UNIT_VALUE[stat] · archetypeMult[stat] / cost
+ 0 · kit / max(kit) kit = Σ_stat |value| · UNIT_VALUE[stat] · kitMult[stat] / cost
+ 0.3 · [is active item]
, × slotBias[archetype][slot]
+ 0.25 · synergy synergy = mean pair win-lift (×10) with items already chosen
(permutation-stats, pairs with ≥ 200 matches only)
-
shrunkWR = (wins + K·mean) / (matches + K)withK = max(200, 20 % of the top item's matches)(Bayesian shrinkage; rare items regress to the hero mean instead of dominating on noisy win rates). The lift is then multiplied bypop: shrinkage handles small samples, not selection bias. An item bought in 5 % of games is bought in games that are already going well, and that bias does not shrink with more matches, so its win rate is only trusted in proportion to how widely it is bought. -
UNIT_VALUE(souls per stat unit,stats.ts) converts stat lines from the assetspropertiesinto a soul-equivalent so value-per-soul is comparable across tiers. Values are hand-set from tier-1 item prices. Both stat terms currently have weight 0: the tuning sweep found they only lower agreement once the population is Phantom+. The table and the kit profile are kept in the code, and the sweep grid includes them, so they come back if a future panel favours them. -
kitMult(kit.ts) comes from the hero assets: number of ability properties that scale withETechPower/ETechDuration, bullet/weapon keywords in ability text, andstandard_level_up_upgrades(bullet-damage vs tech-power growth). Infernus has a documented override: spirit ×1.6, spirit % ×1.5, fire rate ×1.5, ability duration ×1.4, bullet/ability lifesteal ×1.3, cooldown ×1.2, move speed ×1.2 — Afterburn is a bullet-applied burn scaling with spirit, so fire rate and spirit both feed it, and burn duration items extend Afterburn/Napalm. - Game phase: each item's
avg_buy_time_sfor the hero (from item-stats) drives buy order and the early (<10 min) / mid (<22 min) / late grouping. Tier / soul-investment thresholds: at least 4 tier-1 and 3 tier-2 items are forced in, at most 8 items per slot type (4 native slots + 4 flex), at most 3 active items, 16 final items (the game's slot count; minimum 14). An item and the item it upgrades into may both be picked, as in the in-game shop: the component is listed first, the upgrade shares its slot and pays only the cost difference (paidCost). Each component can be upgraded once; at most 6 such upgrade steps are added on top of the 16. - Selection is greedy: repeatedly take the highest-scoring admissible item (ties → lower item id), then sort the chosen set by average buy time (ties → cost, id) and accumulate the running soul total.
From ability-order-stats: sequences restricted to the hero's 4 signature abilities, scored by
shrunkWR × ln(1 + matches); the top sequence is used. Each entry is decoded as
unlock (first appearance) then T1/T2/T3 upgrade tiers. Names and icons come from the abilities asset.
If the build has a passive item that adds ability charges (Extra Charge, Rapid Recharge, Omnicharge
Signet), the sequences come from only the high-rank games where that item was bought (the first such item
in buy order with a sequence of 200+ matches; a style's own games first, then the whole high-rank
population). Those players level the charged abilities much earlier: on Infernus, 99% of Extra Charge
buyers max Flame Dash by their 8th point, against 65% of all games by their 15th. The fetch stores these as
item_ability_order_stats (npm run fetch-data -- --charge-orders-only refreshes just them).